@indielayer/ui 1.16.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/docs/assets/css/tailwind.css +6 -0
- package/docs/components/common/CodePreview.vue +14 -9
- package/docs/components/common/DocsFeatures.vue +41 -0
- package/docs/components/common/DocsHero.vue +216 -0
- package/docs/components/common/DocumentPage.vue +99 -112
- package/docs/components/common/ExampleBlocks.vue +157 -0
- package/docs/components/menu/DocsMenu.vue +3 -0
- package/docs/components/toolbar/Toolbar.vue +11 -2
- package/docs/components/toolbar/ToolbarColorToggle.vue +4 -4
- package/docs/components/toolbar/ToolbarSearch.vue +59 -62
- package/docs/composables/useDocMeta.ts +47 -0
- package/docs/icons.ts +28 -0
- package/docs/layouts/default.vue +1 -3
- package/docs/layouts/simple.vue +3 -1
- package/docs/main.ts +5 -0
- package/docs/pages/colors.vue +56 -47
- package/docs/pages/component/infiniteLoader/composable.vue +168 -0
- package/docs/pages/component/infiniteLoader/index.vue +36 -0
- package/docs/pages/component/infiniteLoader/usage.vue +161 -0
- package/docs/pages/component/select/size.vue +1 -1
- package/docs/pages/component/select/usage.vue +14 -7
- package/docs/pages/component/virtualGrid/index.vue +29 -0
- package/docs/pages/component/virtualGrid/usage.vue +20 -0
- package/docs/pages/component/virtualList/dynamicHeight.vue +75 -0
- package/docs/pages/component/virtualList/index.vue +36 -0
- package/docs/pages/component/virtualList/usage.vue +17 -0
- package/docs/pages/error.vue +5 -3
- package/docs/pages/icons.vue +64 -54
- package/docs/pages/index.vue +93 -82
- package/docs/pages/typography.vue +38 -28
- package/docs/router/index.ts +31 -3
- package/docs/search/components.json +1 -1
- package/docs/search/index.json +1 -0
- package/lib/components/container/theme/Container.base.theme.js +1 -1
- package/lib/components/divider/theme/Divider.base.theme.js +1 -1
- package/lib/components/input/Input.vue.js +23 -24
- package/lib/components/select/Select.vue.d.ts +16 -27
- package/lib/components/select/Select.vue.js +452 -345
- package/lib/components/table/Table.vue.js +1 -1
- package/lib/composables/useVirtualList.d.ts +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +88 -76
- package/lib/index.umd.js +4 -4
- package/lib/install.js +15 -7
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/virtual/components/infiniteLoader/InfiniteLoader.test.d.ts +1 -0
- package/lib/virtual/components/infiniteLoader/InfiniteLoader.vue.d.ts +49 -0
- package/lib/virtual/components/infiniteLoader/InfiniteLoader.vue.js +21 -0
- package/lib/virtual/components/infiniteLoader/InfiniteLoader.vue2.js +4 -0
- package/lib/virtual/components/virtualGrid/VirtualGrid.vue.d.ts +185 -0
- package/lib/virtual/components/virtualGrid/VirtualGrid.vue.js +241 -0
- package/lib/virtual/components/virtualGrid/VirtualGrid.vue2.js +4 -0
- package/lib/virtual/components/virtualGrid/types.d.ts +138 -0
- package/lib/virtual/components/virtualList/VirtualList.test.d.ts +1 -0
- package/lib/virtual/components/virtualList/VirtualList.vue.d.ts +135 -0
- package/lib/virtual/components/virtualList/VirtualList.vue.js +159 -0
- package/lib/virtual/components/virtualList/VirtualList.vue2.js +4 -0
- package/lib/virtual/components/virtualList/isDynamicRowHeight.d.ts +2 -0
- package/lib/virtual/components/virtualList/isDynamicRowHeight.js +6 -0
- package/lib/virtual/components/virtualList/types.d.ts +115 -0
- package/lib/virtual/components/virtualList/useDynamicRowHeight.d.ts +7 -0
- package/lib/virtual/components/virtualList/useDynamicRowHeight.js +68 -0
- package/lib/virtual/components/virtualList/useDynamicRowHeight.test.d.ts +1 -0
- package/lib/virtual/composables/infinite-loader/scanForUnloadedIndices.d.ts +8 -0
- package/lib/virtual/composables/infinite-loader/scanForUnloadedIndices.js +41 -0
- package/lib/virtual/composables/infinite-loader/scanForUnloadedIndices.test.d.ts +1 -0
- package/lib/virtual/composables/infinite-loader/types.d.ts +30 -0
- package/lib/virtual/composables/infinite-loader/useInfiniteLoader.d.ts +6 -0
- package/lib/virtual/composables/infinite-loader/useInfiniteLoader.js +42 -0
- package/lib/virtual/composables/infinite-loader/useInfiniteLoader.test.d.ts +1 -0
- package/lib/virtual/core/createCachedBounds.d.ts +6 -0
- package/lib/virtual/core/createCachedBounds.js +55 -0
- package/lib/virtual/core/getEstimatedSize.d.ts +6 -0
- package/lib/virtual/core/getEstimatedSize.js +22 -0
- package/lib/virtual/core/getOffsetForIndex.d.ts +11 -0
- package/lib/virtual/core/getOffsetForIndex.js +40 -0
- package/lib/virtual/core/getStartStopIndices.d.ts +13 -0
- package/lib/virtual/core/getStartStopIndices.js +31 -0
- package/lib/virtual/core/getStartStopIndices.test.d.ts +1 -0
- package/lib/virtual/core/types.d.ts +11 -0
- package/lib/virtual/core/useCachedBounds.d.ts +7 -0
- package/lib/virtual/core/useCachedBounds.js +18 -0
- package/lib/virtual/core/useIsRtl.d.ts +2 -0
- package/lib/virtual/core/useIsRtl.js +15 -0
- package/lib/virtual/core/useItemSize.d.ts +5 -0
- package/lib/virtual/core/useItemSize.js +27 -0
- package/lib/virtual/core/useVirtualizer.d.ts +33 -0
- package/lib/virtual/core/useVirtualizer.js +171 -0
- package/lib/virtual/index.d.ts +9 -0
- package/lib/virtual/test-utils/mockResizeObserver.d.ts +15 -0
- package/lib/virtual/types.d.ts +2 -0
- package/lib/virtual/utils/adjustScrollOffsetForRtl.d.ts +7 -0
- package/lib/virtual/utils/adjustScrollOffsetForRtl.js +24 -0
- package/lib/virtual/utils/areArraysEqual.d.ts +1 -0
- package/lib/virtual/utils/assert.d.ts +1 -0
- package/lib/virtual/utils/assert.js +7 -0
- package/lib/virtual/utils/getRTLOffsetType.d.ts +2 -0
- package/lib/virtual/utils/getRTLOffsetType.js +13 -0
- package/lib/virtual/utils/getScrollbarSize.d.ts +2 -0
- package/lib/virtual/utils/getScrollbarSize.js +11 -0
- package/lib/virtual/utils/isRtl.d.ts +1 -0
- package/lib/virtual/utils/isRtl.js +12 -0
- package/lib/virtual/utils/parseNumericStyleValue.d.ts +2 -0
- package/lib/virtual/utils/parseNumericStyleValue.js +15 -0
- package/lib/virtual/utils/shallowCompare.d.ts +1 -0
- package/lib/virtual/utils/shallowCompare.js +14 -0
- package/package.json +8 -3
- package/src/components/container/theme/Container.base.theme.ts +1 -1
- package/src/components/divider/theme/Divider.base.theme.ts +1 -1
- package/src/components/input/Input.vue +1 -2
- package/src/components/select/Select.vue +97 -20
- package/src/components/table/Table.vue +1 -1
- package/src/composables/useVirtualList.ts +1 -1
- package/src/index.ts +1 -0
- package/src/install.ts +9 -3
- package/src/version.ts +1 -1
- package/src/virtual/README.md +285 -0
- package/src/virtual/components/infiniteLoader/InfiniteLoader.test.ts +96 -0
- package/src/virtual/components/infiniteLoader/InfiniteLoader.vue +18 -0
- package/src/virtual/components/virtualGrid/VirtualGrid.vue +322 -0
- package/src/virtual/components/virtualGrid/types.ts +160 -0
- package/src/virtual/components/virtualList/VirtualList.test.ts +164 -0
- package/src/virtual/components/virtualList/VirtualList.vue +227 -0
- package/src/virtual/components/virtualList/isDynamicRowHeight.ts +13 -0
- package/src/virtual/components/virtualList/types.ts +127 -0
- package/src/virtual/components/virtualList/useDynamicRowHeight.test.ts +197 -0
- package/src/virtual/components/virtualList/useDynamicRowHeight.ts +149 -0
- package/src/virtual/composables/infinite-loader/scanForUnloadedIndices.test.ts +141 -0
- package/src/virtual/composables/infinite-loader/scanForUnloadedIndices.ts +82 -0
- package/src/virtual/composables/infinite-loader/types.ts +36 -0
- package/src/virtual/composables/infinite-loader/useInfiniteLoader.test.ts +236 -0
- package/src/virtual/composables/infinite-loader/useInfiniteLoader.ts +88 -0
- package/src/virtual/core/createCachedBounds.ts +72 -0
- package/src/virtual/core/getEstimatedSize.ts +29 -0
- package/src/virtual/core/getOffsetForIndex.ts +90 -0
- package/src/virtual/core/getStartStopIndices.test.ts +45 -0
- package/src/virtual/core/getStartStopIndices.ts +71 -0
- package/src/virtual/core/types.ts +17 -0
- package/src/virtual/core/useCachedBounds.ts +21 -0
- package/src/virtual/core/useIsRtl.ts +25 -0
- package/src/virtual/core/useItemSize.ts +34 -0
- package/src/virtual/core/useVirtualizer.ts +294 -0
- package/src/virtual/index.ts +25 -0
- package/src/virtual/test-utils/mockResizeObserver.ts +162 -0
- package/src/virtual/types.ts +3 -0
- package/src/virtual/utils/adjustScrollOffsetForRtl.ts +37 -0
- package/src/virtual/utils/areArraysEqual.ts +13 -0
- package/src/virtual/utils/assert.ts +10 -0
- package/src/virtual/utils/getRTLOffsetType.ts +51 -0
- package/src/virtual/utils/getScrollbarSize.ts +24 -0
- package/src/virtual/utils/isRtl.ts +13 -0
- package/src/virtual/utils/parseNumericStyleValue.ts +21 -0
- package/src/virtual/utils/shallowCompare.ts +29 -0
- package/volar.d.ts +3 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { mount, flushPromises } from '@vue/test-utils'
|
|
3
|
+
import { h, nextTick, type CSSProperties } from 'vue'
|
|
4
|
+
import VirtualList from './VirtualList.vue'
|
|
5
|
+
import { useDynamicRowHeight, DATA_ATTRIBUTE_LIST_INDEX } from './useDynamicRowHeight'
|
|
6
|
+
import { mockResizeObserver, setElementSize } from '../../test-utils/mockResizeObserver'
|
|
7
|
+
import type { DynamicRowHeight } from './types'
|
|
8
|
+
|
|
9
|
+
interface RowSlotProps {
|
|
10
|
+
index: number;
|
|
11
|
+
style: CSSProperties;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const listStyle = { height: '400px' } as const
|
|
15
|
+
|
|
16
|
+
function mountVirtualList(
|
|
17
|
+
props: Record<string, unknown>,
|
|
18
|
+
dynamicRowHeight?: DynamicRowHeight,
|
|
19
|
+
) {
|
|
20
|
+
return mount(VirtualList, {
|
|
21
|
+
props: {
|
|
22
|
+
rowCount: 100,
|
|
23
|
+
rowHeight: dynamicRowHeight ?? 50,
|
|
24
|
+
style: listStyle,
|
|
25
|
+
...props,
|
|
26
|
+
},
|
|
27
|
+
slots: {
|
|
28
|
+
row: ({ index, style }: RowSlotProps) =>
|
|
29
|
+
h('div', { style, class: 'test-row' }, `Row ${index}`),
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createMockDynamicRowHeight(defaultRowHeight = 50) {
|
|
35
|
+
const unobserve = vi.fn<[], void>()
|
|
36
|
+
|
|
37
|
+
const dynamicRowHeight: DynamicRowHeight = {
|
|
38
|
+
getAverageRowHeight: vi.fn(() => defaultRowHeight),
|
|
39
|
+
getRowHeight: vi.fn(() => undefined),
|
|
40
|
+
setRowHeight: vi.fn(),
|
|
41
|
+
observeRowElements: vi.fn(() => unobserve),
|
|
42
|
+
cleanup: vi.fn(),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { dynamicRowHeight, unobserve }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('VirtualList', () => {
|
|
49
|
+
it('renders correctly', () => {
|
|
50
|
+
const wrapper = mountVirtualList({})
|
|
51
|
+
|
|
52
|
+
expect(wrapper.exists()).toBe(true)
|
|
53
|
+
expect(wrapper.attributes('role')).toBe('list')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('renders visible rows', () => {
|
|
57
|
+
const wrapper = mountVirtualList({})
|
|
58
|
+
|
|
59
|
+
const rows = wrapper.findAll('.test-row')
|
|
60
|
+
|
|
61
|
+
expect(rows.length).toBeGreaterThan(0)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('dynamic row height watch', () => {
|
|
65
|
+
it('does not observe row elements when rowHeight is fixed', async () => {
|
|
66
|
+
const { dynamicRowHeight } = createMockDynamicRowHeight()
|
|
67
|
+
|
|
68
|
+
mountVirtualList({ rowHeight: 50 })
|
|
69
|
+
|
|
70
|
+
await flushPromises()
|
|
71
|
+
await nextTick()
|
|
72
|
+
|
|
73
|
+
expect(dynamicRowHeight.observeRowElements).not.toHaveBeenCalled()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('observes visible row elements and sets data-virtual-index', async () => {
|
|
77
|
+
const { dynamicRowHeight } = createMockDynamicRowHeight()
|
|
78
|
+
|
|
79
|
+
const wrapper = mountVirtualList({ rowHeight: dynamicRowHeight })
|
|
80
|
+
|
|
81
|
+
await flushPromises()
|
|
82
|
+
await nextTick()
|
|
83
|
+
|
|
84
|
+
expect(dynamicRowHeight.observeRowElements).toHaveBeenCalled()
|
|
85
|
+
|
|
86
|
+
const observedElements = vi.mocked(dynamicRowHeight.observeRowElements).mock
|
|
87
|
+
.calls[0]?.[0] as Element[]
|
|
88
|
+
|
|
89
|
+
expect(observedElements.length).toBeGreaterThan(0)
|
|
90
|
+
expect(
|
|
91
|
+
observedElements.every((element) => !element.hasAttribute('aria-hidden')),
|
|
92
|
+
).toBe(true)
|
|
93
|
+
|
|
94
|
+
const listElement = wrapper.element as HTMLElement
|
|
95
|
+
const rowElements = Array.from(listElement.children).filter(
|
|
96
|
+
(child) => !child.hasAttribute('aria-hidden'),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
expect(rowElements.length).toBe(observedElements.length)
|
|
100
|
+
rowElements.forEach((element) => {
|
|
101
|
+
expect(element.hasAttribute(DATA_ATTRIBUTE_LIST_INDEX)).toBe(true)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('runs previous unobserve when the observed range changes', async () => {
|
|
106
|
+
const { dynamicRowHeight, unobserve } = createMockDynamicRowHeight()
|
|
107
|
+
|
|
108
|
+
const wrapper = mountVirtualList({ rowHeight: dynamicRowHeight })
|
|
109
|
+
|
|
110
|
+
await flushPromises()
|
|
111
|
+
await nextTick()
|
|
112
|
+
|
|
113
|
+
expect(dynamicRowHeight.observeRowElements).toHaveBeenCalledTimes(1)
|
|
114
|
+
expect(unobserve).not.toHaveBeenCalled()
|
|
115
|
+
|
|
116
|
+
const listElement = wrapper.element as HTMLDivElement
|
|
117
|
+
|
|
118
|
+
listElement.scrollTop = 2500
|
|
119
|
+
listElement.dispatchEvent(new Event('scroll'))
|
|
120
|
+
await flushPromises()
|
|
121
|
+
await nextTick()
|
|
122
|
+
|
|
123
|
+
expect(unobserve).toHaveBeenCalledTimes(1)
|
|
124
|
+
expect(dynamicRowHeight.observeRowElements).toHaveBeenCalledTimes(2)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('dynamic row height integration', () => {
|
|
129
|
+
let unmockResizeObserver: (() => void) | undefined
|
|
130
|
+
let dynamicRowHeight: DynamicRowHeight | undefined
|
|
131
|
+
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
unmockResizeObserver = mockResizeObserver()
|
|
134
|
+
dynamicRowHeight = useDynamicRowHeight({ defaultRowHeight: 50 })
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
afterEach(() => {
|
|
138
|
+
dynamicRowHeight?.cleanup()
|
|
139
|
+
unmockResizeObserver?.()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('updates measured heights when observed rows resize', async () => {
|
|
143
|
+
const wrapper = mountVirtualList({ rowHeight: dynamicRowHeight })
|
|
144
|
+
|
|
145
|
+
await flushPromises()
|
|
146
|
+
await nextTick()
|
|
147
|
+
|
|
148
|
+
const rowElement = wrapper.find('.test-row').element as HTMLElement
|
|
149
|
+
|
|
150
|
+
setElementSize({
|
|
151
|
+
element: rowElement,
|
|
152
|
+
width: 100,
|
|
153
|
+
height: 72,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
await flushPromises()
|
|
157
|
+
await nextTick()
|
|
158
|
+
|
|
159
|
+
const index = Number(rowElement.getAttribute(DATA_ATTRIBUTE_LIST_INDEX))
|
|
160
|
+
|
|
161
|
+
expect(dynamicRowHeight?.getRowHeight(index)).toBe(72)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
})
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export default {
|
|
3
|
+
name: 'XVirtualList',
|
|
4
|
+
}
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { computed, ref, watch, type CSSProperties } from 'vue'
|
|
9
|
+
import { useVirtualizer } from '../../core/useVirtualizer'
|
|
10
|
+
import type { Align } from '../../types'
|
|
11
|
+
import { isDynamicRowHeight as isDynamicRowHeightUtil } from './isDynamicRowHeight'
|
|
12
|
+
import type { VirtualListProps, VirtualListImperativeAPI, DynamicRowHeight } from './types'
|
|
13
|
+
import { DATA_ATTRIBUTE_LIST_INDEX } from './useDynamicRowHeight'
|
|
14
|
+
|
|
15
|
+
const props = withDefaults(defineProps<VirtualListProps>(), {
|
|
16
|
+
defaultHeight: 0,
|
|
17
|
+
overscanCount: 3,
|
|
18
|
+
tag: 'div',
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const element = ref<HTMLDivElement | null>(null)
|
|
22
|
+
|
|
23
|
+
const rowProps = computed(() => props.rowProps || ({} as Record<string, unknown>))
|
|
24
|
+
|
|
25
|
+
const itemCount = computed(() => props.rowCount)
|
|
26
|
+
|
|
27
|
+
const isDynamicRowHeight = computed(() => isDynamicRowHeightUtil(props.rowHeight))
|
|
28
|
+
|
|
29
|
+
const rowHeight = computed<number | string | ((index: number, cellProps: Record<string, unknown>) => number)>(() => {
|
|
30
|
+
if (isDynamicRowHeight.value) {
|
|
31
|
+
const dynamicHeight = props.rowHeight as DynamicRowHeight
|
|
32
|
+
const avgHeight = dynamicHeight.getAverageRowHeight()
|
|
33
|
+
|
|
34
|
+
return (index: number) => {
|
|
35
|
+
return (
|
|
36
|
+
dynamicHeight.getRowHeight(index) ??
|
|
37
|
+
avgHeight
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return props.rowHeight as number | string | ((index: number, cellProps: Record<string, unknown>) => number)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
getCellBounds,
|
|
47
|
+
getEstimatedSize,
|
|
48
|
+
scrollToIndex,
|
|
49
|
+
startIndexOverscan,
|
|
50
|
+
startIndexVisible,
|
|
51
|
+
stopIndexOverscan,
|
|
52
|
+
stopIndexVisible,
|
|
53
|
+
} = useVirtualizer({
|
|
54
|
+
containerElement: element,
|
|
55
|
+
containerStyle: props.style,
|
|
56
|
+
defaultContainerSize: props.defaultHeight,
|
|
57
|
+
direction: 'vertical',
|
|
58
|
+
itemCount,
|
|
59
|
+
itemProps: rowProps,
|
|
60
|
+
itemSize: rowHeight,
|
|
61
|
+
onResize: props.onResize,
|
|
62
|
+
overscanCount: props.overscanCount,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Expose imperative API
|
|
66
|
+
defineExpose<VirtualListImperativeAPI>({
|
|
67
|
+
get element() {
|
|
68
|
+
return element.value
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
scrollToRow({
|
|
72
|
+
align = 'auto',
|
|
73
|
+
behavior = 'auto',
|
|
74
|
+
index,
|
|
75
|
+
}: {
|
|
76
|
+
align?: Align;
|
|
77
|
+
behavior?: ScrollBehavior;
|
|
78
|
+
index: number;
|
|
79
|
+
}) {
|
|
80
|
+
const top = scrollToIndex({
|
|
81
|
+
align,
|
|
82
|
+
containerScrollOffset: element.value?.scrollTop ?? 0,
|
|
83
|
+
index,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
if (typeof element.value?.scrollTo === 'function' && top !== undefined) {
|
|
87
|
+
element.value.scrollTo({
|
|
88
|
+
behavior,
|
|
89
|
+
top,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Watch for dynamic row heights - run after DOM updates
|
|
96
|
+
watch(
|
|
97
|
+
[element, startIndexOverscan, stopIndexOverscan, isDynamicRowHeight, () => props.rowHeight],
|
|
98
|
+
([el, start, stop, isDynamic], _old, onCleanup) => {
|
|
99
|
+
if (!el || !isDynamic) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const rows = Array.from(el.children).filter((item, index) => {
|
|
104
|
+
if (item.hasAttribute('aria-hidden')) {
|
|
105
|
+
// Ignore sizing element
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const attribute = `${start + index}`
|
|
110
|
+
|
|
111
|
+
item.setAttribute(DATA_ATTRIBUTE_LIST_INDEX, attribute)
|
|
112
|
+
|
|
113
|
+
return true
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const dynamicHeight = props.rowHeight as DynamicRowHeight
|
|
117
|
+
|
|
118
|
+
onCleanup(dynamicHeight.observeRowElements(rows))
|
|
119
|
+
},
|
|
120
|
+
{ flush: 'post' }, // Run after DOM updates
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// Notify when visible rows change
|
|
124
|
+
watch(
|
|
125
|
+
[startIndexOverscan, startIndexVisible, stopIndexOverscan, stopIndexVisible],
|
|
126
|
+
([startOverscan, startVisible, stopOverscan, stopVisible]) => {
|
|
127
|
+
if (startOverscan >= 0 && stopOverscan >= 0 && props.onRowsRendered) {
|
|
128
|
+
props.onRowsRendered(
|
|
129
|
+
{
|
|
130
|
+
startIndex: startVisible,
|
|
131
|
+
stopIndex: stopVisible,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
startIndex: startOverscan,
|
|
135
|
+
stopIndex: stopOverscan,
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
interface RowData {
|
|
143
|
+
key: number;
|
|
144
|
+
index: number;
|
|
145
|
+
style: CSSProperties;
|
|
146
|
+
ariaAttributes: {
|
|
147
|
+
'aria-posinset': number;
|
|
148
|
+
'aria-setsize': number;
|
|
149
|
+
role: 'listitem';
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Generate rows
|
|
154
|
+
const rows = computed(() => {
|
|
155
|
+
const result: RowData[] = []
|
|
156
|
+
|
|
157
|
+
if (props.rowCount > 0) {
|
|
158
|
+
for (
|
|
159
|
+
let index = startIndexOverscan.value;
|
|
160
|
+
index <= stopIndexOverscan.value;
|
|
161
|
+
index++
|
|
162
|
+
) {
|
|
163
|
+
const bounds = getCellBounds(index)
|
|
164
|
+
|
|
165
|
+
const rowStyle: CSSProperties = {
|
|
166
|
+
position: 'absolute',
|
|
167
|
+
left: 0,
|
|
168
|
+
transform: `translateY(${bounds.scrollOffset}px)`,
|
|
169
|
+
// In case of dynamic row heights, don't specify a height style
|
|
170
|
+
height: isDynamicRowHeight.value ? undefined : `${bounds.size}px`,
|
|
171
|
+
width: '100%',
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
result.push({
|
|
175
|
+
key: index,
|
|
176
|
+
index,
|
|
177
|
+
style: rowStyle,
|
|
178
|
+
ariaAttributes: {
|
|
179
|
+
'aria-posinset': index + 1,
|
|
180
|
+
'aria-setsize': props.rowCount,
|
|
181
|
+
role: 'listitem',
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result
|
|
188
|
+
})
|
|
189
|
+
</script>
|
|
190
|
+
|
|
191
|
+
<template>
|
|
192
|
+
<component
|
|
193
|
+
:is="tag"
|
|
194
|
+
ref="element"
|
|
195
|
+
:class="$props.class"
|
|
196
|
+
:style="{
|
|
197
|
+
position: 'relative',
|
|
198
|
+
maxHeight: '100%',
|
|
199
|
+
flexGrow: 1,
|
|
200
|
+
overflowY: 'auto',
|
|
201
|
+
...style
|
|
202
|
+
}"
|
|
203
|
+
role="list"
|
|
204
|
+
>
|
|
205
|
+
<template v-for="row in rows" :key="row.key">
|
|
206
|
+
<slot
|
|
207
|
+
name="row"
|
|
208
|
+
:index="row.index"
|
|
209
|
+
:style="row.style"
|
|
210
|
+
:aria-attributes="row.ariaAttributes"
|
|
211
|
+
:props="rowProps"
|
|
212
|
+
></slot>
|
|
213
|
+
</template>
|
|
214
|
+
|
|
215
|
+
<slot ></slot>
|
|
216
|
+
|
|
217
|
+
<!-- Sizing element -->
|
|
218
|
+
<div
|
|
219
|
+
aria-hidden
|
|
220
|
+
:style="{
|
|
221
|
+
height: `${getEstimatedSize}px`,
|
|
222
|
+
width: '100%',
|
|
223
|
+
zIndex: -1
|
|
224
|
+
}"
|
|
225
|
+
></div>
|
|
226
|
+
</component>
|
|
227
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DynamicRowHeight } from './types'
|
|
2
|
+
|
|
3
|
+
export function isDynamicRowHeight(
|
|
4
|
+
value: unknown,
|
|
5
|
+
): value is DynamicRowHeight {
|
|
6
|
+
return (
|
|
7
|
+
typeof value === 'object' &&
|
|
8
|
+
value !== null &&
|
|
9
|
+
'getAverageRowHeight' in value &&
|
|
10
|
+
'getRowHeight' in value &&
|
|
11
|
+
'setRowHeight' in value
|
|
12
|
+
)
|
|
13
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { CSSProperties } from 'vue'
|
|
2
|
+
import type { TagNames } from '../../types'
|
|
3
|
+
|
|
4
|
+
export type DynamicRowHeight = {
|
|
5
|
+
getAverageRowHeight(): number;
|
|
6
|
+
getRowHeight(index: number): number | undefined;
|
|
7
|
+
setRowHeight(index: number, size: number): void;
|
|
8
|
+
observeRowElements: (elements: Element[] | NodeListOf<Element>) => () => void;
|
|
9
|
+
cleanup: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type ForbiddenKeys = 'ariaAttributes' | 'index' | 'style';
|
|
13
|
+
type ExcludeForbiddenKeys<Type> = {
|
|
14
|
+
[Key in keyof Type]: Key extends ForbiddenKeys ? never : Type[Key];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface VirtualListProps {
|
|
18
|
+
/**
|
|
19
|
+
* CSS class name.
|
|
20
|
+
*/
|
|
21
|
+
class?: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Default height of list for initial render.
|
|
25
|
+
* This value is important for server rendering.
|
|
26
|
+
*/
|
|
27
|
+
defaultHeight?: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Callback notified when the List's outermost HTMLElement resizes.
|
|
31
|
+
* This may be used to (re)scroll a row into view.
|
|
32
|
+
*/
|
|
33
|
+
onResize?: (
|
|
34
|
+
size: { height: number; width: number; },
|
|
35
|
+
prevSize: { height: number; width: number; }
|
|
36
|
+
) => void;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Callback notified when the range of visible rows changes.
|
|
40
|
+
*/
|
|
41
|
+
onRowsRendered?: (
|
|
42
|
+
visibleRows: { startIndex: number; stopIndex: number; },
|
|
43
|
+
allRows: { startIndex: number; stopIndex: number; }
|
|
44
|
+
) => void;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* How many additional rows to render outside of the visible area.
|
|
48
|
+
* This can reduce visual flickering near the edges of a list when scrolling.
|
|
49
|
+
*/
|
|
50
|
+
overscanCount?: number;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Number of items to be rendered in the list.
|
|
54
|
+
*/
|
|
55
|
+
rowCount: number;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Row height; the following formats are supported:
|
|
59
|
+
* - number of pixels (number)
|
|
60
|
+
* - percentage of the grid's current height (string)
|
|
61
|
+
* - function that returns the row height (in pixels) given an index and `cellProps`
|
|
62
|
+
* - dynamic row height cache returned by the `useDynamicRowHeight` hook
|
|
63
|
+
*
|
|
64
|
+
* ⚠️ Dynamic row heights are not as efficient as predetermined sizes.
|
|
65
|
+
* It's recommended to provide your own height values if they can be determined ahead of time.
|
|
66
|
+
*/
|
|
67
|
+
rowHeight:
|
|
68
|
+
| number
|
|
69
|
+
| string
|
|
70
|
+
| ((index: number, cellProps: Record<string, unknown>) => number)
|
|
71
|
+
| DynamicRowHeight;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Additional props to be passed to the row-rendering component via slots.
|
|
75
|
+
*/
|
|
76
|
+
rowProps?: ExcludeForbiddenKeys<Record<string, unknown>>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Optional CSS properties.
|
|
80
|
+
* The list of rows will fill the height defined by this style.
|
|
81
|
+
*/
|
|
82
|
+
style?: CSSProperties;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Can be used to override the root HTML element rendered by the List component.
|
|
86
|
+
* The default value is "div", meaning that List renders an HTMLDivElement as its root.
|
|
87
|
+
*
|
|
88
|
+
* ⚠️ In most use cases the default ARIA roles are sufficient and this prop is not needed.
|
|
89
|
+
*/
|
|
90
|
+
tag?: TagNames;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface RowSlotProps {
|
|
94
|
+
ariaAttributes: {
|
|
95
|
+
'aria-posinset': number;
|
|
96
|
+
'aria-setsize': number;
|
|
97
|
+
role: 'listitem';
|
|
98
|
+
};
|
|
99
|
+
index: number;
|
|
100
|
+
style: CSSProperties;
|
|
101
|
+
props?: Record<string, unknown>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Imperative List API.
|
|
106
|
+
*/
|
|
107
|
+
export interface VirtualListImperativeAPI {
|
|
108
|
+
/**
|
|
109
|
+
* Outermost HTML element for the list if mounted and null (if not mounted.
|
|
110
|
+
*/
|
|
111
|
+
readonly element: HTMLDivElement | null;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Scrolls the list so that the specified row is visible.
|
|
115
|
+
*
|
|
116
|
+
* @param align Determines the vertical alignment of the element within the list
|
|
117
|
+
* @param behavior Determines whether scrolling is instant or animates smoothly
|
|
118
|
+
* @param index Index of the row to scroll to (0-based)
|
|
119
|
+
*
|
|
120
|
+
* @throws RangeError if an invalid row index is provided
|
|
121
|
+
*/
|
|
122
|
+
scrollToRow(config: {
|
|
123
|
+
align?: 'auto' | 'center' | 'end' | 'smart' | 'start';
|
|
124
|
+
behavior?: 'auto' | 'instant' | 'smooth';
|
|
125
|
+
index: number;
|
|
126
|
+
}): void;
|
|
127
|
+
}
|