@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.
Files changed (156) hide show
  1. package/README.md +2 -2
  2. package/docs/assets/css/tailwind.css +6 -0
  3. package/docs/components/common/CodePreview.vue +14 -9
  4. package/docs/components/common/DocsFeatures.vue +41 -0
  5. package/docs/components/common/DocsHero.vue +216 -0
  6. package/docs/components/common/DocumentPage.vue +99 -112
  7. package/docs/components/common/ExampleBlocks.vue +157 -0
  8. package/docs/components/menu/DocsMenu.vue +3 -0
  9. package/docs/components/toolbar/Toolbar.vue +11 -2
  10. package/docs/components/toolbar/ToolbarColorToggle.vue +4 -4
  11. package/docs/components/toolbar/ToolbarSearch.vue +59 -62
  12. package/docs/composables/useDocMeta.ts +47 -0
  13. package/docs/icons.ts +28 -0
  14. package/docs/layouts/default.vue +1 -3
  15. package/docs/layouts/simple.vue +3 -1
  16. package/docs/main.ts +5 -0
  17. package/docs/pages/colors.vue +56 -47
  18. package/docs/pages/component/infiniteLoader/composable.vue +168 -0
  19. package/docs/pages/component/infiniteLoader/index.vue +36 -0
  20. package/docs/pages/component/infiniteLoader/usage.vue +161 -0
  21. package/docs/pages/component/select/size.vue +1 -1
  22. package/docs/pages/component/select/usage.vue +14 -7
  23. package/docs/pages/component/virtualGrid/index.vue +29 -0
  24. package/docs/pages/component/virtualGrid/usage.vue +20 -0
  25. package/docs/pages/component/virtualList/dynamicHeight.vue +75 -0
  26. package/docs/pages/component/virtualList/index.vue +36 -0
  27. package/docs/pages/component/virtualList/usage.vue +17 -0
  28. package/docs/pages/error.vue +5 -3
  29. package/docs/pages/icons.vue +64 -54
  30. package/docs/pages/index.vue +93 -82
  31. package/docs/pages/typography.vue +38 -28
  32. package/docs/router/index.ts +31 -3
  33. package/docs/search/components.json +1 -1
  34. package/docs/search/index.json +1 -0
  35. package/lib/components/container/theme/Container.base.theme.js +1 -1
  36. package/lib/components/divider/theme/Divider.base.theme.js +1 -1
  37. package/lib/components/input/Input.vue.js +23 -24
  38. package/lib/components/select/Select.vue.d.ts +16 -27
  39. package/lib/components/select/Select.vue.js +452 -345
  40. package/lib/components/table/Table.vue.js +1 -1
  41. package/lib/composables/useVirtualList.d.ts +1 -1
  42. package/lib/index.d.ts +1 -0
  43. package/lib/index.js +88 -76
  44. package/lib/index.umd.js +4 -4
  45. package/lib/install.js +15 -7
  46. package/lib/version.d.ts +1 -1
  47. package/lib/version.js +1 -1
  48. package/lib/virtual/components/infiniteLoader/InfiniteLoader.test.d.ts +1 -0
  49. package/lib/virtual/components/infiniteLoader/InfiniteLoader.vue.d.ts +49 -0
  50. package/lib/virtual/components/infiniteLoader/InfiniteLoader.vue.js +21 -0
  51. package/lib/virtual/components/infiniteLoader/InfiniteLoader.vue2.js +4 -0
  52. package/lib/virtual/components/virtualGrid/VirtualGrid.vue.d.ts +185 -0
  53. package/lib/virtual/components/virtualGrid/VirtualGrid.vue.js +241 -0
  54. package/lib/virtual/components/virtualGrid/VirtualGrid.vue2.js +4 -0
  55. package/lib/virtual/components/virtualGrid/types.d.ts +138 -0
  56. package/lib/virtual/components/virtualList/VirtualList.test.d.ts +1 -0
  57. package/lib/virtual/components/virtualList/VirtualList.vue.d.ts +135 -0
  58. package/lib/virtual/components/virtualList/VirtualList.vue.js +159 -0
  59. package/lib/virtual/components/virtualList/VirtualList.vue2.js +4 -0
  60. package/lib/virtual/components/virtualList/isDynamicRowHeight.d.ts +2 -0
  61. package/lib/virtual/components/virtualList/isDynamicRowHeight.js +6 -0
  62. package/lib/virtual/components/virtualList/types.d.ts +115 -0
  63. package/lib/virtual/components/virtualList/useDynamicRowHeight.d.ts +7 -0
  64. package/lib/virtual/components/virtualList/useDynamicRowHeight.js +68 -0
  65. package/lib/virtual/components/virtualList/useDynamicRowHeight.test.d.ts +1 -0
  66. package/lib/virtual/composables/infinite-loader/scanForUnloadedIndices.d.ts +8 -0
  67. package/lib/virtual/composables/infinite-loader/scanForUnloadedIndices.js +41 -0
  68. package/lib/virtual/composables/infinite-loader/scanForUnloadedIndices.test.d.ts +1 -0
  69. package/lib/virtual/composables/infinite-loader/types.d.ts +30 -0
  70. package/lib/virtual/composables/infinite-loader/useInfiniteLoader.d.ts +6 -0
  71. package/lib/virtual/composables/infinite-loader/useInfiniteLoader.js +42 -0
  72. package/lib/virtual/composables/infinite-loader/useInfiniteLoader.test.d.ts +1 -0
  73. package/lib/virtual/core/createCachedBounds.d.ts +6 -0
  74. package/lib/virtual/core/createCachedBounds.js +55 -0
  75. package/lib/virtual/core/getEstimatedSize.d.ts +6 -0
  76. package/lib/virtual/core/getEstimatedSize.js +22 -0
  77. package/lib/virtual/core/getOffsetForIndex.d.ts +11 -0
  78. package/lib/virtual/core/getOffsetForIndex.js +40 -0
  79. package/lib/virtual/core/getStartStopIndices.d.ts +13 -0
  80. package/lib/virtual/core/getStartStopIndices.js +31 -0
  81. package/lib/virtual/core/getStartStopIndices.test.d.ts +1 -0
  82. package/lib/virtual/core/types.d.ts +11 -0
  83. package/lib/virtual/core/useCachedBounds.d.ts +7 -0
  84. package/lib/virtual/core/useCachedBounds.js +18 -0
  85. package/lib/virtual/core/useIsRtl.d.ts +2 -0
  86. package/lib/virtual/core/useIsRtl.js +15 -0
  87. package/lib/virtual/core/useItemSize.d.ts +5 -0
  88. package/lib/virtual/core/useItemSize.js +27 -0
  89. package/lib/virtual/core/useVirtualizer.d.ts +33 -0
  90. package/lib/virtual/core/useVirtualizer.js +171 -0
  91. package/lib/virtual/index.d.ts +9 -0
  92. package/lib/virtual/test-utils/mockResizeObserver.d.ts +15 -0
  93. package/lib/virtual/types.d.ts +2 -0
  94. package/lib/virtual/utils/adjustScrollOffsetForRtl.d.ts +7 -0
  95. package/lib/virtual/utils/adjustScrollOffsetForRtl.js +24 -0
  96. package/lib/virtual/utils/areArraysEqual.d.ts +1 -0
  97. package/lib/virtual/utils/assert.d.ts +1 -0
  98. package/lib/virtual/utils/assert.js +7 -0
  99. package/lib/virtual/utils/getRTLOffsetType.d.ts +2 -0
  100. package/lib/virtual/utils/getRTLOffsetType.js +13 -0
  101. package/lib/virtual/utils/getScrollbarSize.d.ts +2 -0
  102. package/lib/virtual/utils/getScrollbarSize.js +11 -0
  103. package/lib/virtual/utils/isRtl.d.ts +1 -0
  104. package/lib/virtual/utils/isRtl.js +12 -0
  105. package/lib/virtual/utils/parseNumericStyleValue.d.ts +2 -0
  106. package/lib/virtual/utils/parseNumericStyleValue.js +15 -0
  107. package/lib/virtual/utils/shallowCompare.d.ts +1 -0
  108. package/lib/virtual/utils/shallowCompare.js +14 -0
  109. package/package.json +8 -3
  110. package/src/components/container/theme/Container.base.theme.ts +1 -1
  111. package/src/components/divider/theme/Divider.base.theme.ts +1 -1
  112. package/src/components/input/Input.vue +1 -2
  113. package/src/components/select/Select.vue +97 -20
  114. package/src/components/table/Table.vue +1 -1
  115. package/src/composables/useVirtualList.ts +1 -1
  116. package/src/index.ts +1 -0
  117. package/src/install.ts +9 -3
  118. package/src/version.ts +1 -1
  119. package/src/virtual/README.md +285 -0
  120. package/src/virtual/components/infiniteLoader/InfiniteLoader.test.ts +96 -0
  121. package/src/virtual/components/infiniteLoader/InfiniteLoader.vue +18 -0
  122. package/src/virtual/components/virtualGrid/VirtualGrid.vue +322 -0
  123. package/src/virtual/components/virtualGrid/types.ts +160 -0
  124. package/src/virtual/components/virtualList/VirtualList.test.ts +164 -0
  125. package/src/virtual/components/virtualList/VirtualList.vue +227 -0
  126. package/src/virtual/components/virtualList/isDynamicRowHeight.ts +13 -0
  127. package/src/virtual/components/virtualList/types.ts +127 -0
  128. package/src/virtual/components/virtualList/useDynamicRowHeight.test.ts +197 -0
  129. package/src/virtual/components/virtualList/useDynamicRowHeight.ts +149 -0
  130. package/src/virtual/composables/infinite-loader/scanForUnloadedIndices.test.ts +141 -0
  131. package/src/virtual/composables/infinite-loader/scanForUnloadedIndices.ts +82 -0
  132. package/src/virtual/composables/infinite-loader/types.ts +36 -0
  133. package/src/virtual/composables/infinite-loader/useInfiniteLoader.test.ts +236 -0
  134. package/src/virtual/composables/infinite-loader/useInfiniteLoader.ts +88 -0
  135. package/src/virtual/core/createCachedBounds.ts +72 -0
  136. package/src/virtual/core/getEstimatedSize.ts +29 -0
  137. package/src/virtual/core/getOffsetForIndex.ts +90 -0
  138. package/src/virtual/core/getStartStopIndices.test.ts +45 -0
  139. package/src/virtual/core/getStartStopIndices.ts +71 -0
  140. package/src/virtual/core/types.ts +17 -0
  141. package/src/virtual/core/useCachedBounds.ts +21 -0
  142. package/src/virtual/core/useIsRtl.ts +25 -0
  143. package/src/virtual/core/useItemSize.ts +34 -0
  144. package/src/virtual/core/useVirtualizer.ts +294 -0
  145. package/src/virtual/index.ts +25 -0
  146. package/src/virtual/test-utils/mockResizeObserver.ts +162 -0
  147. package/src/virtual/types.ts +3 -0
  148. package/src/virtual/utils/adjustScrollOffsetForRtl.ts +37 -0
  149. package/src/virtual/utils/areArraysEqual.ts +13 -0
  150. package/src/virtual/utils/assert.ts +10 -0
  151. package/src/virtual/utils/getRTLOffsetType.ts +51 -0
  152. package/src/virtual/utils/getScrollbarSize.ts +24 -0
  153. package/src/virtual/utils/isRtl.ts +13 -0
  154. package/src/virtual/utils/parseNumericStyleValue.ts +21 -0
  155. package/src/virtual/utils/shallowCompare.ts +29 -0
  156. package/volar.d.ts +3 -0
@@ -0,0 +1,197 @@
1
+ import { describe, expect, test, beforeEach, afterEach } from 'vitest'
2
+ import { ref, nextTick } from 'vue'
3
+ import { useDynamicRowHeight, DATA_ATTRIBUTE_LIST_INDEX } from './useDynamicRowHeight'
4
+ import type { DynamicRowHeight } from './types'
5
+ import { mockResizeObserver, setElementSize } from '../../test-utils/mockResizeObserver'
6
+
7
+ describe('useDynamicRowHeight', () => {
8
+ let unmock: (() => void) | undefined
9
+ let instances: DynamicRowHeight[] = []
10
+
11
+ beforeEach(() => {
12
+ unmock = mockResizeObserver()
13
+ instances = []
14
+ })
15
+
16
+ afterEach(() => {
17
+ instances.forEach((instance) => instance.cleanup())
18
+ if (unmock) {
19
+ unmock()
20
+ }
21
+ })
22
+
23
+ function createDynamicRowHeight(
24
+ ...args: Parameters<typeof useDynamicRowHeight>
25
+ ): DynamicRowHeight {
26
+ const instance = useDynamicRowHeight(...args)
27
+
28
+ instances.push(instance)
29
+
30
+ return instance
31
+ }
32
+
33
+ describe('getAverageRowHeight', () => {
34
+ test('returns an initial estimate based on the defaultRowHeight', () => {
35
+ const dynamicRowHeight = createDynamicRowHeight({
36
+ defaultRowHeight: 100,
37
+ })
38
+
39
+ expect(dynamicRowHeight.getAverageRowHeight()).toBe(100)
40
+ })
41
+
42
+ test('returns an estimate based on measured rows', () => {
43
+ const dynamicRowHeight = createDynamicRowHeight({
44
+ defaultRowHeight: 100,
45
+ })
46
+
47
+ dynamicRowHeight.setRowHeight(0, 10)
48
+ dynamicRowHeight.setRowHeight(1, 20)
49
+ expect(dynamicRowHeight.getAverageRowHeight()).toBe(15)
50
+
51
+ dynamicRowHeight.setRowHeight(2, 30)
52
+ expect(dynamicRowHeight.getAverageRowHeight()).toBe(20)
53
+
54
+ dynamicRowHeight.setRowHeight(2, 15)
55
+ expect(dynamicRowHeight.getAverageRowHeight()).toBe(15)
56
+ })
57
+
58
+ test('resets when key changes', async () => {
59
+ const key = ref('a')
60
+ const dynamicRowHeight = createDynamicRowHeight({
61
+ defaultRowHeight: 100,
62
+ key,
63
+ })
64
+
65
+ dynamicRowHeight.setRowHeight(0, 10)
66
+ expect(dynamicRowHeight.getAverageRowHeight()).toBe(10)
67
+
68
+ // Key hasn't changed
69
+ await nextTick()
70
+ expect(dynamicRowHeight.getAverageRowHeight()).toBe(10)
71
+
72
+ // Change key
73
+ key.value = 'b'
74
+ await nextTick()
75
+ expect(dynamicRowHeight.getAverageRowHeight()).toBe(100)
76
+ })
77
+ })
78
+
79
+ describe('getRowHeight', () => {
80
+ test('returns estimated height for a row that has not yet been measured', () => {
81
+ const dynamicRowHeight = createDynamicRowHeight({
82
+ defaultRowHeight: 100,
83
+ })
84
+
85
+ expect(dynamicRowHeight.getRowHeight(0)).toBe(100)
86
+ })
87
+
88
+ test('returns the most recently measured size', () => {
89
+ const dynamicRowHeight = createDynamicRowHeight({
90
+ defaultRowHeight: 100,
91
+ })
92
+
93
+ dynamicRowHeight.setRowHeight(0, 15)
94
+ dynamicRowHeight.setRowHeight(1, 20)
95
+ dynamicRowHeight.setRowHeight(3, 25)
96
+ expect(dynamicRowHeight.getRowHeight(0)).toBe(15)
97
+ expect(dynamicRowHeight.getRowHeight(1)).toBe(20)
98
+ expect(dynamicRowHeight.getRowHeight(2)).toBe(100)
99
+ expect(dynamicRowHeight.getRowHeight(3)).toBe(25)
100
+
101
+ dynamicRowHeight.setRowHeight(1, 25)
102
+ expect(dynamicRowHeight.getRowHeight(1)).toBe(25)
103
+ })
104
+
105
+ test('resets when key changes', async () => {
106
+ const key = ref('a')
107
+ const dynamicRowHeight = createDynamicRowHeight({
108
+ defaultRowHeight: 100,
109
+ key,
110
+ })
111
+
112
+ dynamicRowHeight.setRowHeight(0, 10)
113
+ expect(dynamicRowHeight.getRowHeight(0)).toBe(10)
114
+
115
+ // Key hasn't changed
116
+ await nextTick()
117
+ expect(dynamicRowHeight.getRowHeight(0)).toBe(10)
118
+
119
+ // Change key
120
+ key.value = 'b'
121
+ await nextTick()
122
+ expect(dynamicRowHeight.getRowHeight(0)).toBe(100)
123
+ })
124
+ })
125
+
126
+ describe('observeRowElements', () => {
127
+ function createRowElement(index: number) {
128
+ const element = document.createElement('div')
129
+
130
+ element.setAttribute(DATA_ATTRIBUTE_LIST_INDEX, '' + index)
131
+
132
+ return element
133
+ }
134
+
135
+ test('should update cache when an observed element is resized', async () => {
136
+ const dynamicRowHeight = createDynamicRowHeight({
137
+ defaultRowHeight: 100,
138
+ })
139
+
140
+ const elementA = createRowElement(0)
141
+ const elementB = createRowElement(1)
142
+
143
+ dynamicRowHeight.observeRowElements([elementA, elementB])
144
+ await nextTick()
145
+ expect(dynamicRowHeight.getRowHeight(0)).toBe(100)
146
+ expect(dynamicRowHeight.getRowHeight(1)).toBe(100)
147
+
148
+ setElementSize({
149
+ element: elementB,
150
+ width: 100,
151
+ height: 20,
152
+ })
153
+ await nextTick()
154
+ expect(dynamicRowHeight.getRowHeight(0)).toBe(100)
155
+ expect(dynamicRowHeight.getRowHeight(1)).toBe(20)
156
+
157
+ setElementSize({
158
+ element: elementA,
159
+ width: 100,
160
+ height: 15,
161
+ })
162
+ await nextTick()
163
+ expect(dynamicRowHeight.getRowHeight(0)).toBe(15)
164
+ expect(dynamicRowHeight.getRowHeight(1)).toBe(20)
165
+ })
166
+
167
+ test('should unobserve an element when requested', async () => {
168
+ const dynamicRowHeight = createDynamicRowHeight({
169
+ defaultRowHeight: 100,
170
+ })
171
+
172
+ const element = createRowElement(0)
173
+
174
+ const unobserve = dynamicRowHeight.observeRowElements([element])
175
+
176
+ setElementSize({
177
+ element,
178
+ width: 100,
179
+ height: 10,
180
+ })
181
+ await nextTick()
182
+ expect(dynamicRowHeight.getRowHeight(0)).toBe(10)
183
+
184
+ unobserve()
185
+
186
+ setElementSize({
187
+ element,
188
+ width: 100,
189
+ height: 20,
190
+ })
191
+ await nextTick()
192
+ expect(dynamicRowHeight.getRowHeight(0)).toBe(10)
193
+ })
194
+ })
195
+
196
+ // setRowHeight is tested indirectly by "getAverageRowHeight" and "getRowHeight" blocks above
197
+ })
@@ -0,0 +1,149 @@
1
+ import { getCurrentInstance, onBeforeUnmount, ref, watch, type Ref } from 'vue'
2
+ import { assert } from '../../utils/assert'
3
+ import type { DynamicRowHeight } from './types'
4
+
5
+ export const DATA_ATTRIBUTE_LIST_INDEX = 'data-virtual-index'
6
+
7
+ export function useDynamicRowHeight({
8
+ defaultRowHeight,
9
+ key,
10
+ }: {
11
+ defaultRowHeight: number;
12
+ key?: Ref<string | number | undefined> | string | number;
13
+ }): DynamicRowHeight {
14
+ const map = ref<Map<number, number>>(new Map())
15
+ // Revision counter to trigger reactivity when map changes
16
+ const revision = ref(0)
17
+
18
+ // Handle reactive key changes
19
+ if (key !== undefined && typeof key === 'object' && 'value' in key) {
20
+ watch(key as Ref<string | number | undefined>, () => {
21
+ map.value = new Map()
22
+ revision.value++
23
+ })
24
+ }
25
+
26
+ const getAverageRowHeight = () => {
27
+ // Access revision to track dependency
28
+ revision.value
29
+
30
+ let totalHeight = 0
31
+
32
+ map.value.forEach((height) => {
33
+ totalHeight += height
34
+ })
35
+
36
+ if (totalHeight === 0) {
37
+ return defaultRowHeight
38
+ }
39
+
40
+ return totalHeight / map.value.size
41
+ }
42
+
43
+ const getRowHeight = (index: number) => {
44
+ // Access revision to track dependency
45
+ revision.value
46
+
47
+ const measuredHeight = map.value.get(index)
48
+
49
+ if (measuredHeight !== undefined) {
50
+ return measuredHeight
51
+ }
52
+
53
+ // Don't cache default height here - let ResizeObserver do the measuring
54
+ // This prevents stale default heights from blocking updates
55
+ return defaultRowHeight
56
+ }
57
+
58
+ const setRowHeight = (index: number, size: number) => {
59
+ if (map.value.get(index) === size) {
60
+ return
61
+ }
62
+
63
+ // Create a new Map to trigger reactivity
64
+ const newMap = new Map(map.value)
65
+
66
+ newMap.set(index, size)
67
+ map.value = newMap
68
+ revision.value++
69
+ }
70
+
71
+ const resizeObserverCallback = (entries: ResizeObserverEntry[]) => {
72
+ if (entries.length === 0) {
73
+ return
74
+ }
75
+
76
+ // Batch updates to avoid triggering multiple recalculations
77
+ let hasChanges = false
78
+ const updates: Array<{ index: number; height: number; }> = []
79
+
80
+ entries.forEach((entry) => {
81
+ const { borderBoxSize, target } = entry
82
+
83
+ const attribute = target.getAttribute(DATA_ATTRIBUTE_LIST_INDEX)
84
+
85
+ assert(
86
+ attribute !== null,
87
+ `Invalid ${DATA_ATTRIBUTE_LIST_INDEX} attribute value`,
88
+ )
89
+
90
+ const index = parseInt(attribute)
91
+
92
+ const { blockSize: height } = borderBoxSize[0]
93
+
94
+ if (!height) {
95
+ // Ignore heights that have not yet been measured (e.g. <img> elements that have not yet loaded)
96
+ return
97
+ }
98
+
99
+ // Check if height actually changed before updating
100
+ const currentHeight = map.value.get(index)
101
+
102
+ if (currentHeight !== height) {
103
+ updates.push({ index, height })
104
+ hasChanges = true
105
+ }
106
+ })
107
+
108
+ // Apply all updates at once
109
+ if (hasChanges) {
110
+ const newMap = new Map(map.value)
111
+
112
+ updates.forEach(({ index, height }) => {
113
+ newMap.set(index, height)
114
+ })
115
+ map.value = newMap
116
+ revision.value++
117
+ }
118
+ }
119
+
120
+ const resizeObserver = new ResizeObserver(resizeObserverCallback)
121
+
122
+ function cleanup() {
123
+ resizeObserver.disconnect()
124
+ }
125
+
126
+ if (getCurrentInstance()) {
127
+ onBeforeUnmount(cleanup)
128
+ }
129
+
130
+ const observeRowElements = (elements: Element[] | NodeListOf<Element>) => {
131
+ const elementsArray = Array.isArray(elements)
132
+ ? elements
133
+ : Array.from(elements)
134
+
135
+ elementsArray.forEach((element) => resizeObserver.observe(element))
136
+
137
+ return () => {
138
+ elementsArray.forEach((element) => resizeObserver.unobserve(element))
139
+ }
140
+ }
141
+
142
+ return {
143
+ getAverageRowHeight,
144
+ getRowHeight,
145
+ setRowHeight,
146
+ observeRowElements,
147
+ cleanup,
148
+ }
149
+ }
@@ -0,0 +1,141 @@
1
+ import { describe, expect, test, vi } from 'vitest'
2
+ import { scanForUnloadedIndices } from './scanForUnloadedIndices'
3
+
4
+ describe('scanForUnloadedIndices', () => {
5
+ test('should return an empty array for a range of rows that have all been loaded', () => {
6
+ const isRowLoaded = vi.fn((index: number) => {
7
+ expect(index).toBeGreaterThanOrEqual(0)
8
+ expect(index).toBeLessThanOrEqual(2)
9
+
10
+ return true
11
+ })
12
+
13
+ expect(
14
+ scanForUnloadedIndices({
15
+ minimumBatchSize: 0,
16
+ isRowLoaded,
17
+ rowCount: 3,
18
+ startIndex: 0,
19
+ stopIndex: 2,
20
+ }),
21
+ ).toEqual([])
22
+
23
+ expect(isRowLoaded).toHaveBeenCalledTimes(3)
24
+ })
25
+
26
+ test('return a range of only 1 unloaded row', () => {
27
+ const isRowLoaded = vi.fn((index: number) => {
28
+ expect(index).toBeGreaterThanOrEqual(0)
29
+ expect(index).toBeLessThanOrEqual(2)
30
+
31
+ return index !== 1
32
+ })
33
+
34
+ expect(
35
+ scanForUnloadedIndices({
36
+ minimumBatchSize: 0,
37
+ isRowLoaded,
38
+ rowCount: 3,
39
+ startIndex: 0,
40
+ stopIndex: 2,
41
+ }),
42
+ ).toEqual([{ startIndex: 1, stopIndex: 1 }])
43
+
44
+ expect(isRowLoaded).toHaveBeenCalledTimes(3)
45
+ })
46
+
47
+ test('return a range of multiple unloaded rows', () => {
48
+ const isRowLoaded = vi.fn((index: number) => {
49
+ expect(index).toBeGreaterThanOrEqual(0)
50
+ expect(index).toBeLessThanOrEqual(2)
51
+
52
+ return index === 2
53
+ })
54
+
55
+ expect(
56
+ scanForUnloadedIndices({
57
+ minimumBatchSize: 0,
58
+ isRowLoaded,
59
+ rowCount: 3,
60
+ startIndex: 0,
61
+ stopIndex: 2,
62
+ }),
63
+ ).toEqual([{ startIndex: 0, stopIndex: 1 }])
64
+
65
+ expect(isRowLoaded).toHaveBeenCalledTimes(3)
66
+ })
67
+
68
+ test('return multiple ranges of unloaded rows', () => {
69
+ const isRowLoaded = vi.fn((index: number) => {
70
+ expect(index).toBeGreaterThanOrEqual(0)
71
+ expect(index).toBeLessThanOrEqual(6)
72
+ switch (index) {
73
+ case 0:
74
+ case 3:
75
+ case 5: {
76
+ return true
77
+ }
78
+ }
79
+
80
+ return false
81
+ })
82
+
83
+ expect(
84
+ scanForUnloadedIndices({
85
+ minimumBatchSize: 0,
86
+ isRowLoaded,
87
+ rowCount: 7,
88
+ startIndex: 0,
89
+ stopIndex: 6,
90
+ }),
91
+ ).toEqual([
92
+ { startIndex: 1, stopIndex: 2 },
93
+ { startIndex: 4, stopIndex: 4 },
94
+ { startIndex: 6, stopIndex: 6 },
95
+ ])
96
+
97
+ expect(isRowLoaded).toHaveBeenCalledTimes(7)
98
+ })
99
+
100
+ test('return respect the minimum batch size param when fetching rows ahead', () => {
101
+ const isRowLoaded = vi.fn((index: number) => {
102
+ expect(index).toBeGreaterThanOrEqual(0)
103
+ expect(index).toBeLessThanOrEqual(9)
104
+
105
+ return index < 4
106
+ })
107
+
108
+ expect(
109
+ scanForUnloadedIndices({
110
+ minimumBatchSize: 4,
111
+ isRowLoaded,
112
+ rowCount: 10,
113
+ startIndex: 0,
114
+ stopIndex: 4,
115
+ }),
116
+ ).toEqual([{ startIndex: 4, stopIndex: 7 }])
117
+
118
+ expect(isRowLoaded).toHaveBeenCalledTimes(8)
119
+ })
120
+
121
+ test('return respect the minimum batch size param when fetching rows behind', () => {
122
+ const isRowLoaded = vi.fn((index: number) => {
123
+ expect(index).toBeGreaterThanOrEqual(0)
124
+ expect(index).toBeLessThanOrEqual(9)
125
+
126
+ return index > 6
127
+ })
128
+
129
+ expect(
130
+ scanForUnloadedIndices({
131
+ minimumBatchSize: 4,
132
+ isRowLoaded,
133
+ rowCount: 10,
134
+ startIndex: 6,
135
+ stopIndex: 9,
136
+ }),
137
+ ).toEqual([{ startIndex: 3, stopIndex: 6 }])
138
+
139
+ expect(isRowLoaded).toHaveBeenCalledTimes(7)
140
+ })
141
+ })
@@ -0,0 +1,82 @@
1
+ import type { Indices } from './types'
2
+
3
+ export function scanForUnloadedIndices({
4
+ isRowLoaded,
5
+ minimumBatchSize,
6
+ rowCount,
7
+ startIndex,
8
+ stopIndex,
9
+ }: {
10
+ isRowLoaded: (index: number) => boolean;
11
+ minimumBatchSize: number;
12
+ rowCount: number;
13
+ startIndex: number;
14
+ stopIndex: number;
15
+ }): Indices[] {
16
+ const indices: Indices[] = []
17
+
18
+ let currentStartIndex = -1
19
+ let currentStopIndex = -1
20
+
21
+ for (let index = startIndex; index <= stopIndex; index++) {
22
+ if (!isRowLoaded(index)) {
23
+ currentStopIndex = index
24
+ if (currentStartIndex < 0) {
25
+ currentStartIndex = index
26
+ }
27
+ } else if (currentStopIndex >= 0) {
28
+ indices.push({
29
+ startIndex: currentStartIndex,
30
+ stopIndex: currentStopIndex,
31
+ })
32
+
33
+ currentStartIndex = currentStopIndex = -1
34
+ }
35
+ }
36
+
37
+ // Scan forward to satisfy the minimum batch size.
38
+ if (currentStopIndex >= 0) {
39
+ const potentialStopIndex = Math.min(
40
+ Math.max(currentStopIndex, currentStartIndex + minimumBatchSize - 1),
41
+ rowCount - 1,
42
+ )
43
+
44
+ for (
45
+ let index = currentStopIndex + 1;
46
+ index <= potentialStopIndex;
47
+ index++
48
+ ) {
49
+ if (!isRowLoaded(index)) {
50
+ currentStopIndex = index
51
+ } else {
52
+ break
53
+ }
54
+ }
55
+
56
+ indices.push({
57
+ startIndex: currentStartIndex,
58
+ stopIndex: currentStopIndex,
59
+ })
60
+ }
61
+
62
+ // Check to see if our first range ended prematurely.
63
+ // In this case we should scan backwards to satisfy the minimum batch size.
64
+ if (indices.length) {
65
+ const firstIndices = indices[0]
66
+
67
+ while (
68
+ firstIndices.stopIndex - firstIndices.startIndex + 1 < minimumBatchSize &&
69
+ firstIndices.startIndex > 0
70
+ ) {
71
+ const index = firstIndices.startIndex - 1
72
+
73
+ if (!isRowLoaded(index)) {
74
+ firstIndices.startIndex = index
75
+ } else {
76
+ break
77
+ }
78
+ }
79
+ }
80
+
81
+ return indices
82
+ }
@@ -0,0 +1,36 @@
1
+ export type Indices = {
2
+ startIndex: number;
3
+ stopIndex: number;
4
+ };
5
+
6
+ export type OnRowsRendered = (indices: Indices) => void;
7
+
8
+ export type InfiniteLoaderProps = {
9
+ /**
10
+ * Function responsible for tracking the loaded state of each item.
11
+ */
12
+ isRowLoaded: (index: number) => boolean;
13
+
14
+ /**
15
+ * Callback to be invoked when more rows must be loaded.
16
+ * It should return a Promise that is resolved once all data has finished loading.
17
+ */
18
+ loadMoreRows: (startIndex: number, stopIndex: number) => Promise<void>;
19
+
20
+ /**
21
+ * Minimum number of rows to be loaded at a time; defaults to 10.
22
+ * This property can be used to batch requests to reduce HTTP requests.
23
+ */
24
+ minimumBatchSize?: number;
25
+
26
+ /**
27
+ * Threshold at which to pre-fetch data; defaults to 15.
28
+ * A threshold of 15 means that data will start loading when a user scrolls within 15 rows.
29
+ */
30
+ threshold?: number;
31
+
32
+ /**
33
+ * Number of rows in list; can be arbitrary high number if actual number is unknown.
34
+ */
35
+ rowCount: number;
36
+ };