Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cdk:scroll): add simulated scroll support to VirtualScroll (#1812)
- Loading branch information
Showing
51 changed files
with
1,134 additions
and
72 deletions.
There are no files selected for viewing
12 changes: 8 additions & 4 deletions
12
packages/cdk/scroll/__tests__/__snapshots__/virtualScroll.spec.ts.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,25 @@ | ||
// Vitest Snapshot v1 | ||
|
||
exports[`VirtualScroll > basic work > render work 1`] = ` | ||
"<div class=\\"cdk-virtual-scroll\\"> | ||
<div class=\\"cdk-virtual-scroll-holder\\" style=\\"max-height: 200px; width: 100%;\\"> | ||
"<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-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> | ||
</div> | ||
<!----> | ||
<!----> | ||
</div>" | ||
`; | ||
exports[`VirtualScroll > basic work > rowRender work 1`] = ` | ||
"<div class=\\"cdk-virtual-scroll\\"> | ||
<div class=\\"cdk-virtual-scroll-holder\\" style=\\"max-height: 200px; width: 100%;\\"> | ||
"<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-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> | ||
</div> | ||
<!----> | ||
<!----> | ||
</div>" | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
--- | ||
title: | ||
zh: 模拟滚动 | ||
en: Simulated scroll | ||
order: 4 | ||
--- | ||
|
||
## zh | ||
|
||
使用模拟滚动替代原生滚动。 | ||
|
||
## en | ||
|
||
Use simulated scroll instead of native scroll. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
<template> | ||
<div class="demo-both-scroll-wrapper"> | ||
<CdkVirtualScroll | ||
ref="listRef" | ||
:dataSource="rowData" | ||
:height="200" | ||
:rowHeight="20" | ||
:colWidth="200" | ||
:virtual="true" | ||
:rowRender="rowRender" | ||
getKey="key" | ||
> | ||
<template #col="{ row, item, index }"> | ||
<span class="virtual-item" @click="onItemClick(item.key)">{{ row.key }} - {{ index }}</span> | ||
</template> | ||
</CdkVirtualScroll> | ||
</div> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { h, ref } from 'vue' | ||
import { VirtualRowRenderFn, VirtualScrollInstance, VirtualScrollRowData } from '@idux/cdk/scroll' | ||
const listRef = ref<VirtualScrollInstance>() | ||
const colData: { key: string }[] = [] | ||
for (let index = 0; index < 1000; index++) { | ||
colData.push({ key: `col-key-${index}` }) | ||
} | ||
const rowData: VirtualScrollRowData[] = [] | ||
for (let index = 0; index < 1000; index++) { | ||
rowData.push({ | ||
key: `row-key-${index}`, | ||
data: colData, | ||
}) | ||
} | ||
const rowRender: VirtualRowRenderFn = ({ children }) => | ||
h( | ||
'div', | ||
{ | ||
class: 'virtual-row', | ||
}, | ||
children, | ||
) | ||
const onItemClick = (key: string) => { | ||
console.log('click:', key) | ||
} | ||
</script> | ||
|
||
<style lang="less"> | ||
.demo-both-scroll-wrapper { | ||
height: 240px; | ||
.cdk-virtual-scroll { | ||
border: 1px solid red; | ||
margin-bottom: 8px; | ||
} | ||
.virtual-row { | ||
flex-shrink: 0; | ||
display: flex; | ||
height: 20px; | ||
flex-wrap: nowrap; | ||
border: 1px solid gray; | ||
} | ||
.virtual-item { | ||
flex-shrink: 0; | ||
padding-left: 16px; | ||
border: 1px solid gray; | ||
height: 100%; | ||
width: 200px; | ||
line-height: 18px; | ||
} | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/** | ||
* @license | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE | ||
*/ | ||
|
||
import { computed, defineComponent, normalizeStyle, ref } from 'vue' | ||
|
||
import { convertCssPixel } from '@idux/cdk/utils' | ||
|
||
import { useScrollbarState } from './composables/useScrollbarState' | ||
import { scrollbarProps } from './types' | ||
|
||
export default defineComponent({ | ||
props: scrollbarProps, | ||
setup(props) { | ||
const scrollbarRef = ref<HTMLElement>() | ||
const thumbRef = ref<HTMLElement>() | ||
|
||
const { canScroll, offset, isDragging, thumbSize } = useScrollbarState(props, scrollbarRef, thumbRef) | ||
|
||
const classes = computed(() => ({ | ||
'cdk-scrollbar': true, | ||
'cdk-scrollbar-vertical': !props.horizontal, | ||
'cdk-scrollbar-horizontal': !!props.horizontal, | ||
})) | ||
const style = computed(() => normalizeStyle(props.containerStyle)) | ||
const thumbClass = computed(() => ({ | ||
'cdk-scrollbar-thumb': true, | ||
'cdk-scrollbar-thumb-moving': isDragging.value, | ||
})) | ||
const thumbStyle = computed(() => { | ||
const thumbSizeStyle = convertCssPixel(thumbSize.value) | ||
const offsetSizeStyle = convertCssPixel(offset.value) | ||
const _style = props.horizontal | ||
? { | ||
marginLeft: offsetSizeStyle, | ||
width: thumbSizeStyle, | ||
} | ||
: { | ||
marginTop: offsetSizeStyle, | ||
height: thumbSizeStyle, | ||
} | ||
|
||
return normalizeStyle([_style, props.thumbStyle]) | ||
}) | ||
|
||
return () => ( | ||
<div ref={scrollbarRef} class={classes.value} style={style.value}> | ||
<div v-show={canScroll.value} ref={thumbRef} class={thumbClass.value} style={thumbStyle.value} /> | ||
</div> | ||
) | ||
}, | ||
}) |
125 changes: 125 additions & 0 deletions
125
packages/cdk/scroll/src/scrollbar/composables/useScrollbarState.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
/** | ||
* @license | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE | ||
*/ | ||
|
||
import type { ScrollbarProps } from '../types' | ||
|
||
import { type ComputedRef, type Ref, computed, onBeforeUnmount, onMounted } from 'vue' | ||
|
||
import { callEmit, cancelRAF, rAF, useEventListener, useState } from '@idux/cdk/utils' | ||
|
||
export interface ScrollbarStateContext { | ||
canScroll: ComputedRef<boolean> | ||
offset: ComputedRef<number> | ||
thumbSize: ComputedRef<number> | ||
isDragging: ComputedRef<boolean> | ||
} | ||
|
||
export function useScrollbarState( | ||
props: ScrollbarProps, | ||
scrollbarRef: Ref<HTMLElement | undefined>, | ||
thumbRef: Ref<HTMLElement | undefined>, | ||
): ScrollbarStateContext { | ||
const thumbSize = computed(() => getThumbSize(props.thumbMinSize, props.containerSize, props.scrollRange)) | ||
|
||
const enabledScrollRange = computed(() => Math.max(props.scrollRange - props.containerSize, 0)) | ||
const enabledOffsetRange = computed(() => Math.max(props.containerSize - thumbSize.value, 0)) | ||
const offset = computed(() => { | ||
if (props.scrollOffset === 0 || enabledOffsetRange.value === 0) { | ||
return 0 | ||
} | ||
|
||
return (props.scrollOffset / enabledScrollRange.value) * enabledOffsetRange.value | ||
}) | ||
const canScroll = computed(() => enabledScrollRange.value > 0) | ||
|
||
const [isDragging, setIsDragging] = useState(false) | ||
const [startOffset, setStartOffset] = useState(0) | ||
const [pageXY, setPageXY] = useState(0) | ||
let rafId: number | ||
|
||
const onThumbMouseDown = (evt: MouseEvent | TouchEvent) => { | ||
setIsDragging(true) | ||
setPageXY(getPageXY(evt, props.horizontal)) | ||
setStartOffset(offset.value) | ||
|
||
callEmit(props.onMoveStart) | ||
evt.stopPropagation() | ||
evt.preventDefault() | ||
} | ||
|
||
const onMouseMove = (evt: MouseEvent | TouchEvent) => { | ||
cancelRAF(rafId) | ||
if (!isDragging.value) { | ||
return | ||
} | ||
|
||
const _enabledScrollRange = enabledScrollRange.value | ||
const movedOffset = getPageXY(evt, props.horizontal) - pageXY.value | ||
const newOffset = startOffset.value + movedOffset | ||
|
||
const ptg = _enabledScrollRange ? newOffset / enabledOffsetRange.value : 0 | ||
|
||
let newScrollOffset = Math.ceil(ptg * _enabledScrollRange) | ||
newScrollOffset = Math.max(newScrollOffset, 0) | ||
newScrollOffset = Math.min(newScrollOffset, _enabledScrollRange) | ||
|
||
rafId = rAF(() => { | ||
callEmit(props.onScroll, newScrollOffset) | ||
}) | ||
} | ||
const onMouseUp = () => { | ||
setIsDragging(false) | ||
callEmit(props.onMoveEnd) | ||
} | ||
|
||
const onScrollbarTouchStart = (evt: TouchEvent) => { | ||
evt.preventDefault() | ||
} | ||
|
||
let listenerStops: (() => void)[] = [] | ||
const clearListeners = () => { | ||
listenerStops.forEach(stop => stop()) | ||
} | ||
|
||
onMounted(() => { | ||
listenerStops = [ | ||
useEventListener(scrollbarRef, 'touchstart', onScrollbarTouchStart), | ||
useEventListener(thumbRef, 'touchstart', onThumbMouseDown), | ||
useEventListener(thumbRef, 'mousedown', onThumbMouseDown), | ||
useEventListener(window, 'mousemove', onMouseMove), | ||
useEventListener(window, 'touchmove', onMouseMove), | ||
useEventListener(window, 'mouseup', onMouseUp), | ||
useEventListener(window, 'touchend', onMouseUp), | ||
] | ||
}) | ||
onBeforeUnmount(() => { | ||
clearListeners() | ||
cancelRAF(rafId) | ||
}) | ||
|
||
return { | ||
canScroll, | ||
offset, | ||
thumbSize, | ||
isDragging, | ||
} | ||
} | ||
|
||
function getPageXY(evt: MouseEvent | TouchEvent, horizontal: boolean) { | ||
const pageData = 'touches' in evt ? evt.touches[0] : evt | ||
return pageData[horizontal ? 'pageX' : 'pageY'] | ||
} | ||
|
||
function getThumbSize(thumbMinSize: number, containerSize = 0, scrollRange = 0) { | ||
let baseSize = (containerSize / scrollRange) * containerSize | ||
if (isNaN(baseSize)) { | ||
baseSize = 0 | ||
} | ||
baseSize = Math.max(baseSize, thumbMinSize) | ||
baseSize = Math.min(baseSize, containerSize / 2) | ||
return Math.floor(baseSize) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
/** | ||
* @license | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE | ||
*/ | ||
|
||
import type { ScrollbarComponent } from './types' | ||
|
||
import Scrollbar from './Scrollbar' | ||
|
||
const CdkScrollbar = Scrollbar as ScrollbarComponent | ||
|
||
export { CdkScrollbar } | ||
|
||
export type { ScrollbarComponent, ScrollBarInstance, ScrollbarPublicProps as ScrollbarProps } from './types' |
Oops, something went wrong.