virtual-scroller 1.8.1 → 1.9.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 (247) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +337 -160
  3. package/bundle/virtual-scroller-dom.js +1 -1
  4. package/bundle/virtual-scroller-dom.js.map +1 -1
  5. package/bundle/virtual-scroller-react.js +1 -1
  6. package/bundle/virtual-scroller-react.js.map +1 -1
  7. package/bundle/virtual-scroller.js +1 -1
  8. package/bundle/virtual-scroller.js.map +1 -1
  9. package/commonjs/BeforeResize.js +23 -27
  10. package/commonjs/BeforeResize.js.map +1 -1
  11. package/commonjs/DOM/Engine.js +7 -7
  12. package/commonjs/DOM/Engine.js.map +1 -1
  13. package/commonjs/DOM/ItemsContainer.js +1 -1
  14. package/commonjs/DOM/ItemsContainer.js.map +1 -1
  15. package/commonjs/DOM/ListTopOffsetWatcher.js +15 -9
  16. package/commonjs/DOM/ListTopOffsetWatcher.js.map +1 -1
  17. package/commonjs/DOM/ScrollableContainer.js +28 -28
  18. package/commonjs/DOM/ScrollableContainer.js.map +1 -1
  19. package/commonjs/DOM/VirtualScroller.js +20 -17
  20. package/commonjs/DOM/VirtualScroller.js.map +1 -1
  21. package/commonjs/DOM/tbody.js +16 -10
  22. package/commonjs/DOM/tbody.js.map +1 -1
  23. package/commonjs/ItemHeights.js +13 -7
  24. package/commonjs/ItemHeights.js.map +1 -1
  25. package/commonjs/Layout.js +15 -13
  26. package/commonjs/Layout.js.map +1 -1
  27. package/commonjs/Layout.test.js +8 -3
  28. package/commonjs/Layout.test.js.map +1 -1
  29. package/commonjs/{ListHeightChangeWatcher.js → ListHeightMeasurement.js} +26 -28
  30. package/commonjs/ListHeightMeasurement.js.map +1 -0
  31. package/commonjs/Resize.js +38 -28
  32. package/commonjs/Resize.js.map +1 -1
  33. package/commonjs/Scroll.js +28 -44
  34. package/commonjs/Scroll.js.map +1 -1
  35. package/commonjs/VirtualScroller.columns.js +43 -0
  36. package/commonjs/VirtualScroller.columns.js.map +1 -0
  37. package/commonjs/VirtualScroller.constructor.js +408 -0
  38. package/commonjs/VirtualScroller.constructor.js.map +1 -0
  39. package/commonjs/VirtualScroller.items.js +305 -0
  40. package/commonjs/VirtualScroller.items.js.map +1 -0
  41. package/commonjs/VirtualScroller.js +132 -1872
  42. package/commonjs/VirtualScroller.js.map +1 -1
  43. package/commonjs/VirtualScroller.layout.js +562 -0
  44. package/commonjs/VirtualScroller.layout.js.map +1 -0
  45. package/commonjs/VirtualScroller.onRender.js +357 -0
  46. package/commonjs/VirtualScroller.onRender.js.map +1 -0
  47. package/commonjs/VirtualScroller.resize.js +186 -0
  48. package/commonjs/VirtualScroller.resize.js.map +1 -0
  49. package/commonjs/VirtualScroller.state.js +301 -0
  50. package/commonjs/VirtualScroller.state.js.map +1 -0
  51. package/commonjs/VirtualScroller.verticalSpacing.js +65 -0
  52. package/commonjs/VirtualScroller.verticalSpacing.js.map +1 -0
  53. package/commonjs/getItemCoordinates.js.map +1 -1
  54. package/commonjs/getItemsDiff.js.map +1 -1
  55. package/commonjs/getVerticalSpacing.js.map +1 -1
  56. package/commonjs/package.json +5 -0
  57. package/commonjs/react/VirtualScroller.js +180 -620
  58. package/commonjs/react/VirtualScroller.js.map +1 -1
  59. package/commonjs/react/useClassName.js +26 -0
  60. package/commonjs/react/useClassName.js.map +1 -0
  61. package/commonjs/react/useHandleItemsChange.js +116 -0
  62. package/commonjs/react/useHandleItemsChange.js.map +1 -0
  63. package/commonjs/react/useInstanceMethods.js +37 -0
  64. package/commonjs/react/useInstanceMethods.js.map +1 -0
  65. package/commonjs/react/useItemKeys.js +60 -0
  66. package/commonjs/react/useItemKeys.js.map +1 -0
  67. package/commonjs/react/useOnItemHeightChange.js +32 -0
  68. package/commonjs/react/useOnItemHeightChange.js.map +1 -0
  69. package/commonjs/react/useOnItemStateChange.js +32 -0
  70. package/commonjs/react/useOnItemStateChange.js.map +1 -0
  71. package/commonjs/react/useState.js +140 -0
  72. package/commonjs/react/useState.js.map +1 -0
  73. package/commonjs/react/useStyle.js +29 -0
  74. package/commonjs/react/useStyle.js.map +1 -0
  75. package/commonjs/react/useVirtualScroller.js +62 -0
  76. package/commonjs/react/useVirtualScroller.js.map +1 -0
  77. package/commonjs/react/useVirtualScrollerStartStop.js +20 -0
  78. package/commonjs/react/useVirtualScrollerStartStop.js.map +1 -0
  79. package/commonjs/test/Engine.js +23 -0
  80. package/commonjs/test/Engine.js.map +1 -0
  81. package/commonjs/test/ItemsContainer.js +127 -0
  82. package/commonjs/test/ItemsContainer.js.map +1 -0
  83. package/commonjs/test/ScrollableContainer.js +130 -0
  84. package/commonjs/test/ScrollableContainer.js.map +1 -0
  85. package/commonjs/test/VirtualScroller.js +281 -0
  86. package/commonjs/test/VirtualScroller.js.map +1 -0
  87. package/commonjs/utility/debounce.js +2 -2
  88. package/commonjs/utility/debounce.js.map +1 -1
  89. package/commonjs/utility/debug.js.map +1 -1
  90. package/commonjs/utility/getStateSnapshot.js +2 -2
  91. package/commonjs/utility/getStateSnapshot.js.map +1 -1
  92. package/commonjs/utility/px.js.map +1 -1
  93. package/commonjs/utility/px.test.js +1 -1
  94. package/commonjs/utility/px.test.js.map +1 -1
  95. package/commonjs/utility/shallowEqual.js +1 -1
  96. package/commonjs/utility/shallowEqual.js.map +1 -1
  97. package/commonjs/utility/throttle.js.map +1 -1
  98. package/dom/index.cjs +4 -0
  99. package/dom/index.cjs.js +9 -0
  100. package/dom/index.d.ts +6 -4
  101. package/dom/index.js +1 -1
  102. package/dom/package.json +10 -4
  103. package/index.cjs +4 -0
  104. package/index.cjs.js +9 -0
  105. package/index.d.ts +30 -15
  106. package/index.js +1 -1
  107. package/modules/BeforeResize.js +22 -27
  108. package/modules/BeforeResize.js.map +1 -1
  109. package/modules/DOM/Engine.js +6 -6
  110. package/modules/DOM/Engine.js.map +1 -1
  111. package/modules/DOM/ItemsContainer.js +1 -1
  112. package/modules/DOM/ItemsContainer.js.map +1 -1
  113. package/modules/DOM/ListTopOffsetWatcher.js +15 -9
  114. package/modules/DOM/ListTopOffsetWatcher.js.map +1 -1
  115. package/modules/DOM/ScrollableContainer.js +28 -28
  116. package/modules/DOM/ScrollableContainer.js.map +1 -1
  117. package/modules/DOM/VirtualScroller.js +19 -16
  118. package/modules/DOM/VirtualScroller.js.map +1 -1
  119. package/modules/DOM/tbody.js +11 -9
  120. package/modules/DOM/tbody.js.map +1 -1
  121. package/modules/ItemHeights.js +12 -6
  122. package/modules/ItemHeights.js.map +1 -1
  123. package/modules/Layout.js +14 -12
  124. package/modules/Layout.js.map +1 -1
  125. package/modules/Layout.test.js +8 -3
  126. package/modules/Layout.test.js.map +1 -1
  127. package/modules/{ListHeightChangeWatcher.js → ListHeightMeasurement.js} +25 -27
  128. package/modules/ListHeightMeasurement.js.map +1 -0
  129. package/modules/Resize.js +38 -28
  130. package/modules/Resize.js.map +1 -1
  131. package/modules/Scroll.js +28 -44
  132. package/modules/Scroll.js.map +1 -1
  133. package/modules/VirtualScroller.columns.js +36 -0
  134. package/modules/VirtualScroller.columns.js.map +1 -0
  135. package/modules/VirtualScroller.constructor.js +371 -0
  136. package/modules/VirtualScroller.constructor.js.map +1 -0
  137. package/modules/VirtualScroller.items.js +288 -0
  138. package/modules/VirtualScroller.items.js.map +1 -0
  139. package/modules/VirtualScroller.js +132 -1860
  140. package/modules/VirtualScroller.js.map +1 -1
  141. package/modules/VirtualScroller.layout.js +549 -0
  142. package/modules/VirtualScroller.layout.js.map +1 -0
  143. package/modules/VirtualScroller.onRender.js +337 -0
  144. package/modules/VirtualScroller.onRender.js.map +1 -0
  145. package/modules/VirtualScroller.resize.js +176 -0
  146. package/modules/VirtualScroller.resize.js.map +1 -0
  147. package/modules/VirtualScroller.state.js +283 -0
  148. package/modules/VirtualScroller.state.js.map +1 -0
  149. package/modules/VirtualScroller.verticalSpacing.js +54 -0
  150. package/modules/VirtualScroller.verticalSpacing.js.map +1 -0
  151. package/modules/getItemCoordinates.js.map +1 -1
  152. package/modules/getItemsDiff.js.map +1 -1
  153. package/modules/getVerticalSpacing.js.map +1 -1
  154. package/modules/react/VirtualScroller.js +176 -625
  155. package/modules/react/VirtualScroller.js.map +1 -1
  156. package/modules/react/useClassName.js +18 -0
  157. package/modules/react/useClassName.js.map +1 -0
  158. package/modules/react/useHandleItemsChange.js +108 -0
  159. package/modules/react/useHandleItemsChange.js.map +1 -0
  160. package/modules/react/useInstanceMethods.js +28 -0
  161. package/modules/react/useInstanceMethods.js.map +1 -0
  162. package/modules/react/useItemKeys.js +52 -0
  163. package/modules/react/useItemKeys.js.map +1 -0
  164. package/modules/react/useOnItemHeightChange.js +24 -0
  165. package/modules/react/useOnItemHeightChange.js.map +1 -0
  166. package/modules/react/useOnItemStateChange.js +24 -0
  167. package/modules/react/useOnItemStateChange.js.map +1 -0
  168. package/modules/react/useState.js +132 -0
  169. package/modules/react/useState.js.map +1 -0
  170. package/modules/react/useStyle.js +19 -0
  171. package/modules/react/useStyle.js.map +1 -0
  172. package/modules/react/useVirtualScroller.js +51 -0
  173. package/modules/react/useVirtualScroller.js.map +1 -0
  174. package/modules/react/useVirtualScrollerStartStop.js +12 -0
  175. package/modules/react/useVirtualScrollerStartStop.js.map +1 -0
  176. package/modules/test/Engine.js +11 -0
  177. package/modules/test/Engine.js.map +1 -0
  178. package/modules/test/ItemsContainer.js +120 -0
  179. package/modules/test/ItemsContainer.js.map +1 -0
  180. package/modules/test/ScrollableContainer.js +123 -0
  181. package/modules/test/ScrollableContainer.js.map +1 -0
  182. package/modules/test/VirtualScroller.js +270 -0
  183. package/modules/test/VirtualScroller.js.map +1 -0
  184. package/modules/utility/debounce.js +2 -2
  185. package/modules/utility/debounce.js.map +1 -1
  186. package/modules/utility/debug.js.map +1 -1
  187. package/modules/utility/getStateSnapshot.js +2 -2
  188. package/modules/utility/getStateSnapshot.js.map +1 -1
  189. package/modules/utility/px.js.map +1 -1
  190. package/modules/utility/px.test.js +1 -1
  191. package/modules/utility/px.test.js.map +1 -1
  192. package/modules/utility/shallowEqual.js +1 -1
  193. package/modules/utility/shallowEqual.js.map +1 -1
  194. package/modules/utility/throttle.js.map +1 -1
  195. package/package.json +46 -23
  196. package/react/index.cjs +4 -0
  197. package/react/index.cjs.js +9 -0
  198. package/react/index.d.ts +10 -9
  199. package/react/index.js +1 -1
  200. package/react/package.json +10 -4
  201. package/rollup.config.mjs +62 -0
  202. package/runnable/create-commonjs-package-json.js +11 -0
  203. package/source/BeforeResize.js +16 -21
  204. package/source/DOM/Engine.js +8 -10
  205. package/source/DOM/ListTopOffsetWatcher.js +13 -8
  206. package/source/DOM/ScrollableContainer.js +23 -21
  207. package/source/DOM/VirtualScroller.js +27 -11
  208. package/source/DOM/tbody.js +30 -21
  209. package/source/ItemHeights.js +9 -4
  210. package/source/Layout.js +12 -9
  211. package/source/Layout.test.js +8 -3
  212. package/source/{ListHeightChangeWatcher.js → ListHeightMeasurement.js} +21 -20
  213. package/source/Resize.js +41 -25
  214. package/source/Scroll.js +27 -35
  215. package/source/VirtualScroller.columns.js +26 -0
  216. package/source/VirtualScroller.constructor.js +336 -0
  217. package/source/VirtualScroller.items.js +302 -0
  218. package/source/VirtualScroller.js +144 -1875
  219. package/source/VirtualScroller.layout.js +539 -0
  220. package/source/VirtualScroller.onRender.js +345 -0
  221. package/source/VirtualScroller.resize.js +189 -0
  222. package/source/VirtualScroller.state.js +284 -0
  223. package/source/VirtualScroller.verticalSpacing.js +51 -0
  224. package/source/react/VirtualScroller.js +243 -587
  225. package/source/react/useClassName.js +14 -0
  226. package/source/react/useHandleItemsChange.js +115 -0
  227. package/source/react/useInstanceMethods.js +25 -0
  228. package/source/react/useItemKeys.js +59 -0
  229. package/source/react/useOnItemHeightChange.js +28 -0
  230. package/source/react/useOnItemStateChange.js +28 -0
  231. package/source/react/useState.js +114 -0
  232. package/source/react/useStyle.js +20 -0
  233. package/source/react/useVirtualScroller.js +59 -0
  234. package/source/react/useVirtualScrollerStartStop.js +12 -0
  235. package/source/test/Engine.js +11 -0
  236. package/source/test/ItemsContainer.js +87 -0
  237. package/source/test/ScrollableContainer.js +88 -0
  238. package/source/test/VirtualScroller.js +232 -0
  239. package/source/utility/debounce.js +2 -2
  240. package/source/utility/px.test.js +1 -1
  241. package/babel.config.js +0 -25
  242. package/babel.js +0 -5
  243. package/commonjs/ListHeightChangeWatcher.js.map +0 -1
  244. package/dom/index.commonjs.js +0 -4
  245. package/index.commonjs.js +0 -4
  246. package/modules/ListHeightChangeWatcher.js.map +0 -1
  247. package/react/index.commonjs.js +0 -4
@@ -0,0 +1,14 @@
1
+ import { TBODY_CLASS_NAME } from '../DOM/tbody.js'
2
+
3
+ export default function useClassName(className, { tbody }) {
4
+ // For `<tbody/>`, a workaround is used which uses CSS variables
5
+ // and a special CSS class name "VirtualScroller".
6
+ // See `addTbodyStyles()` function in `../DOM/tbody.js` for more details.
7
+ if (tbody) {
8
+ if (className) {
9
+ return className + ' ' + TBODY_CLASS_NAME
10
+ }
11
+ return TBODY_CLASS_NAME
12
+ }
13
+ return className
14
+ }
@@ -0,0 +1,115 @@
1
+ import { useRef, useLayoutEffect } from 'react'
2
+
3
+ // If new `items` are passed:
4
+ //
5
+ // * Store the scroll Y position for the first one of the current items
6
+ // so that it could potentially (in some cases) be restored after the
7
+ // new `items` are rendered.
8
+ //
9
+ // * Call `VirtualScroller.setItems()` function.
10
+ //
11
+ // * Re-generate the React `key` prefix for item elements
12
+ // so that all item components are re-rendered for the new `items` list.
13
+ // That's because item components may have their own internal state,
14
+ // and simply passing another `item` property for an item component
15
+ // might result in bugs, which React would do with its "re-using" policy
16
+ // if the unique `key` workaround hasn't been used.
17
+ //
18
+ export default function useHandleItemsChange(items, {
19
+ virtualScroller,
20
+ // `preserveScrollPosition` property name is deprecated,
21
+ // use `preserveScrollPositionOnPrependItems` property instead.
22
+ preserveScrollPosition,
23
+ preserveScrollPositionOnPrependItems,
24
+ updateItemKeysForNewItems
25
+ }) {
26
+ const {
27
+ items: renderedItems,
28
+ firstShownItemIndex
29
+ } = virtualScroller.getState()
30
+
31
+ // During render, check if the `items` list has changed.
32
+ // If it has, capture the Y scroll position and updated item element `key`s.
33
+
34
+ // A long "advanced" sidenote on why capturing scroll Y position
35
+ // is done during render instead of in an "effect":
36
+ //
37
+ // Previously, capturing scroll Y position was being done in `useLayoutEffect()`
38
+ // but it was later found out that it wouldn't work for a "Show previous" button
39
+ // scenario because that button would get hidden by the time `useLayoutEffect()`
40
+ // gets called when there're no more "previous" items to show.
41
+ //
42
+ // Consider this code example:
43
+ //
44
+ // const { fromIndex, items } = this.state
45
+ // const items = allItems.slice(fromIndex)
46
+ // return (
47
+ // {fromIndex > 0 &&
48
+ // <button onClick={this.onShowPrevious}>
49
+ // Show previous
50
+ // </button>
51
+ // }
52
+ // <VirtualScroller
53
+ // items={items}
54
+ // itemComponent={ItemComponent}/>
55
+ // )
56
+ //
57
+ // Consider a user clicks "Show previous" to show the items from the start.
58
+ // By the time `componentDidUpdate()` is called on `<VirtualScroller/>`,
59
+ // the "Show previous" button has already been hidden
60
+ // (because there're no more "previous" items)
61
+ // which results in the scroll Y position jumping forward
62
+ // by the height of that "Show previous" button.
63
+ // This is because `<VirtualScroller/>` captures scroll Y
64
+ // position when items are prepended via `.setItems()`
65
+ // when the "Show previous" button is still being shown,
66
+ // and then restores scroll Y position in `.onRender()`
67
+ // when the "Show previous" button has already been hidden:
68
+ // that's the reason for the scroll Y "jump".
69
+ //
70
+ // To prevent that, scroll Y position is captured at `render()`
71
+ // time rather than later in `componentDidUpdate()`: this way,
72
+ // scroll Y position is captured while the "Show previous" button
73
+ // is still being shown.
74
+
75
+ const previousItems = useRef(items)
76
+ const hasItemsPropertyChanged = items !== previousItems.current
77
+ previousItems.current = items
78
+ if (hasItemsPropertyChanged) {
79
+ let itemsHaveChanged = true
80
+ let shouldUpdateItemKeys = true
81
+ const itemsDiff = virtualScroller.getItemsDiff(renderedItems, items)
82
+ // `itemsDiff` will be `undefined` in case of a non-incremental items list change.
83
+ if (itemsDiff) {
84
+ const {
85
+ prependedItemsCount,
86
+ appendedItemsCount
87
+ } = itemsDiff
88
+ if (prependedItemsCount === 0 && appendedItemsCount === 0) {
89
+ // The items haven't changed. No need to re-generate
90
+ // the `key` prefix or to snapshot the Y scroll position.
91
+ itemsHaveChanged = false
92
+ shouldUpdateItemKeys = false
93
+ }
94
+ else if (prependedItemsCount === 0 && appendedItemsCount > 0) {
95
+ // Just some items got appended. No need to re-generate
96
+ // the `key` prefix or to snapshot the Y scroll position.
97
+ shouldUpdateItemKeys = false
98
+ }
99
+ }
100
+
101
+ if (itemsHaveChanged) {
102
+ // Set the new `items`.
103
+ virtualScroller.setItems(items, {
104
+ // `preserveScrollPosition` property name is deprecated,
105
+ // use `preserveScrollPositionOnPrependItems` property instead.
106
+ preserveScrollPositionOnPrependItems: preserveScrollPositionOnPrependItems || preserveScrollPosition
107
+ })
108
+
109
+ // Update React element `key`s for the new set of `items`.
110
+ if (shouldUpdateItemKeys) {
111
+ updateItemKeysForNewItems()
112
+ }
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,25 @@
1
+ import { useImperativeHandle } from 'react'
2
+
3
+ import { reportError } from '../utility/debug.js'
4
+
5
+ // Adds instance methods to the React component.
6
+ export default function useInstanceMethods(ref, {
7
+ virtualScroller
8
+ }) {
9
+ useImperativeHandle(ref, () => ({
10
+ // This is a proxy for `VirtualScroller`'s `.updateLayout` instance method.
11
+ updateLayout: () => virtualScroller.updateLayout(),
12
+
13
+ // (deprecated)
14
+ // `.layout()` method name is deprecated, use `.updateLayout()` instead.
15
+ layout: () => virtualScroller.updateLayout(),
16
+
17
+ // (deprecated)
18
+ updateItem: (i) => reportError(`[virtual-scroller] ".updateItem(i)" method of React <VirtualScroller/> has been removed`),
19
+
20
+ // (deprecated)
21
+ renderItem: (i) => reportError(`[virtual-scroller] ".renderItem(i)" method of React <VirtualScroller/> has been removed`)
22
+ }), [
23
+ virtualScroller
24
+ ])
25
+ }
@@ -0,0 +1,59 @@
1
+ import { useRef, useMemo, useCallback } from 'react'
2
+
3
+ export default function useItemKeys({ getItemId }) {
4
+ // List items are rendered with `key`s so that React doesn't
5
+ // "reuse" `itemComponent`s in cases when `items` are changed.
6
+ const itemKeyPrefix = useRef()
7
+
8
+ // Generates a unique `key` prefix for list item components.
9
+ const generateItemKeyPrefix = useMemo(() => {
10
+ let counter = 0
11
+ function getNextCounter() {
12
+ if (counter === Number.MAX_SAFE_INTEGER) {
13
+ counter = 0
14
+ }
15
+ counter++
16
+ return counter
17
+ }
18
+ return () => {
19
+ itemKeyPrefix.current = String(getNextCounter())
20
+ }
21
+ }, [
22
+ itemKeyPrefix
23
+ ])
24
+
25
+ useMemo(() => {
26
+ // Generate an initial unique `key` prefix for list item components.
27
+ generateItemKeyPrefix()
28
+ }, [])
29
+
30
+ const generateItemKeyPrefixIfNotUsingItemIds = useCallback(() => {
31
+ if (!getItemId) {
32
+ generateItemKeyPrefix()
33
+ }
34
+ }, [
35
+ getItemId,
36
+ generateItemKeyPrefix
37
+ ])
38
+
39
+ /**
40
+ * Returns a `key` for an `item`'s element.
41
+ * @param {object} item — The item.
42
+ * @param {number} i — Item's index in `items` list.
43
+ * @return {any}
44
+ */
45
+ const getItemKey = useCallback((item, i) => {
46
+ if (getItemId) {
47
+ return getItemId(item)
48
+ }
49
+ return `${itemKeyPrefix.current}:${i}`
50
+ }, [
51
+ getItemId,
52
+ itemKeyPrefix
53
+ ])
54
+
55
+ return {
56
+ getItemKey,
57
+ updateItemKeysForNewItems: generateItemKeyPrefixIfNotUsingItemIds
58
+ }
59
+ }
@@ -0,0 +1,28 @@
1
+ import { useMemo, useRef, useCallback } from 'react'
2
+
3
+ export default function useOnItemHeightChange({
4
+ items,
5
+ virtualScroller
6
+ }) {
7
+ // Only compute the initial cache value once.
8
+ const initialCacheValue = useMemo(() => {
9
+ return new Array(items.length)
10
+ }, [])
11
+
12
+ // Handler functions cache.
13
+ const cache = useRef(initialCacheValue)
14
+
15
+ // Caches per-item `onItemHeightChange` functions' "references"
16
+ // so that item components don't get re-rendered needlessly.
17
+ const getOnItemHeightChange = useCallback((i) => {
18
+ if (!cache.current[i]) {
19
+ cache.current[i] = () => virtualScroller.onItemHeightChange(i)
20
+ }
21
+ return cache.current[i]
22
+ }, [
23
+ virtualScroller,
24
+ cache
25
+ ])
26
+
27
+ return getOnItemHeightChange
28
+ }
@@ -0,0 +1,28 @@
1
+ import { useMemo, useRef, useCallback } from 'react'
2
+
3
+ export default function useOnItemStateChange({
4
+ items,
5
+ virtualScroller
6
+ }) {
7
+ // Only compute the initial cache value once.
8
+ const initialCacheValue = useMemo(() => {
9
+ return new Array(items.length)
10
+ }, [])
11
+
12
+ // Handler functions cache.
13
+ const cache = useRef(initialCacheValue)
14
+
15
+ // Caches per-item `onItemStateChange` functions' "references"
16
+ // so that item components don't get re-rendered needlessly.
17
+ const getOnItemStateChange = useCallback((i) => {
18
+ if (!cache.current[i]) {
19
+ cache.current[i] = (itemState) => virtualScroller.onItemStateChange(i, itemState)
20
+ }
21
+ return cache.current[i]
22
+ }, [
23
+ virtualScroller,
24
+ cache
25
+ ])
26
+
27
+ return getOnItemStateChange
28
+ }
@@ -0,0 +1,114 @@
1
+ import { useState, useRef, useLayoutEffect } from 'react'
2
+
3
+ // Creates state management functions.
4
+ export default function _useState({ initialState, onRender, items }) {
5
+ // `VirtualScroller` state.
6
+ //
7
+ // The `_stateUpdate` variable shouldn't be used directly
8
+ // because in some cases its value may not represent
9
+ // the actual `state` of the `VirtualScroller`.
10
+ //
11
+ // * It will contain an incorrect initial value if `initialState` property is passed
12
+ // because it doesn't get initialized to `initialState`.
13
+ //
14
+ // * If `items` property gets changed, `state` reference variable gets updated immediately
15
+ // but the `_stateUpdate` variable here doesn't (until the component re-renders some other time).
16
+ //
17
+ // Instead, use the `state` reference below.
18
+ //
19
+ const [_stateUpdate, _setStateUpdate] = useState()
20
+
21
+ // This `state` reference is used for accessing the externally stored
22
+ // virtual scroller state from inside a `VirtualScroller` instance.
23
+ //
24
+ // It's also the "source of truth" on the actual `VirtualScroller` state.
25
+ //
26
+ const state = useRef(initialState)
27
+
28
+ // Accumulates state updates until they have been applied.
29
+ const targetState = useRef(initialState)
30
+
31
+ // Update the current state reference.
32
+ //
33
+ // Ignores the cases when `state` reference has already been updated
34
+ // "immediately" bypassing a `_setStateUpdate()` call, because
35
+ // in that case, `_stateUpdate` holds a stale value.
36
+ //
37
+ if (state.current !== targetState.current) {
38
+ state.current = _stateUpdate
39
+ }
40
+
41
+ // Call `onRender()` right after every state update.
42
+ //
43
+ // When `items` property changes, `useHandleItemsChange()` hook doesn't call
44
+ // `_setStateUpdate()` because there's no need for a re-render.
45
+ // But chaning `items` still does trigger a `VirtualScroller` state update,
46
+ // so added `items` property in the list of this "effect"'s dependencies.
47
+ //
48
+ useLayoutEffect(() => {
49
+ onRender()
50
+ }, [
51
+ _stateUpdate,
52
+ items
53
+ ])
54
+
55
+ return {
56
+ getState: () => state.current,
57
+
58
+ // Updates existing state.
59
+ //
60
+ // State updates are incremental meaning that this code should mimick
61
+ // the classic `React.Component`'s `this.setState()` behavior
62
+ // when calling `this.setState()` doesn't replace `state` but rather merges
63
+ // a set of the updated state properties with the rest of the old ones.
64
+ //
65
+ // The reason is that `useState()` updates are "asynchronous" (not immediate),
66
+ // and simply merging over `...state` would merge over potentially stale
67
+ // property values in cases when more than a single `updateState()` call is made
68
+ // before the state actually updates, resulting in losing some of the state updates.
69
+ //
70
+ // For example, the first `updateState()` call updates shown item indexes,
71
+ // and the second `updateState()` call updates `verticalSpacing`.
72
+ // If it was simply `updateState({ ...state, ...stateUpdate })`
73
+ // then the second state update could overwrite the first state update,
74
+ // resulting in incorrect items being shown/hidden.
75
+ //
76
+ // Using `...state.current` instead of `...pendingState.current` here
77
+ // would produce "stale" results.
78
+ //
79
+ updateState: (stateUpdate) => {
80
+ const newState = {
81
+ ...targetState.current,
82
+ ...stateUpdate
83
+ }
84
+ targetState.current = newState
85
+ // If `items` property did change the component detects it at render time
86
+ // and updates `VirtualScroller` items immediately by calling `.setItems()`.
87
+ // But, since all of that happens at render time and not in an "effect",
88
+ // if the state update was done as usual by calling `_setStateUpdate()`,
89
+ // React would throw an error about updating state during render.
90
+ // Hence, state update in that particular case should happen "directly",
91
+ // without waiting for an "asynchronous" effect to trigger and call
92
+ // an "asyncronous" `_setStateUpdate()` function.
93
+ //
94
+ // Updating state directly in that particular case works because there
95
+ // already is a render ongoing, so there's no need to re-render the component
96
+ // again after such render-time state update.
97
+ //
98
+ // When the initial `VirtualScroller` state is being set, it contains an `.items`
99
+ // property too, but that initial setting is done using another function called
100
+ // `setInitialState()`, so using `if (stateUpdate.items)` condition here for describing
101
+ // just the case when `state` has been updated as a result of a `setItems()` call
102
+ // seems to be fine.
103
+ //
104
+ if (stateUpdate.items) {
105
+ // If a `stateUpdate` contains `items` then it means that there was a `setItems()` call.
106
+ // No need to trigger a re-render — the component got re-rendered anyway.
107
+ // Just update the `state` "in place".
108
+ state.current = newState
109
+ } else {
110
+ _setStateUpdate(newState)
111
+ }
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,20 @@
1
+ import px from '../utility/px.js'
2
+
3
+ export default function useStyle({
4
+ tbody,
5
+ virtualScroller
6
+ }) {
7
+ if (tbody) {
8
+ return
9
+ }
10
+
11
+ const {
12
+ beforeItemsHeight,
13
+ afterItemsHeight
14
+ } = virtualScroller.getState()
15
+
16
+ return {
17
+ paddingTop: px(beforeItemsHeight),
18
+ paddingBottom: px(afterItemsHeight)
19
+ }
20
+ }
@@ -0,0 +1,59 @@
1
+ import { useMemo } from 'react'
2
+
3
+ import VirtualScroller from '../VirtualScroller.js'
4
+
5
+ // Creates a `VirtualScroller` instance.
6
+ export default function useVirtualScroller({
7
+ items,
8
+ estimatedItemHeight,
9
+ bypass,
10
+ // bypassBatchSize,
11
+ tbody,
12
+ onItemInitialRender,
13
+ // `onItemFirstRender(i)` is deprecated, use `onItemInitialRender(item)` instead.
14
+ onItemFirstRender,
15
+ initialScrollPosition,
16
+ onScrollPositionChange,
17
+ measureItemsBatchSize,
18
+ // `scrollableContainer` property is deprecated.
19
+ // Use `getScrollableContainer()` property instead.
20
+ scrollableContainer,
21
+ getScrollableContainer,
22
+ getColumnsCount,
23
+ getItemId,
24
+ AsComponent,
25
+ initialState,
26
+ onStateChange
27
+ }, {
28
+ container
29
+ }) {
30
+ return useMemo(() => {
31
+ // Create `virtual-scroller` instance.
32
+ return new VirtualScroller(
33
+ () => container.current,
34
+ items,
35
+ {
36
+ _useTimeoutInRenderLoop: true,
37
+ estimatedItemHeight,
38
+ bypass,
39
+ // bypassBatchSize,
40
+ tbody,
41
+ onItemInitialRender,
42
+ // `onItemFirstRender(i)` is deprecated, use `onItemInitialRender(item)` instead.
43
+ onItemFirstRender,
44
+ initialScrollPosition,
45
+ onScrollPositionChange,
46
+ measureItemsBatchSize,
47
+ // `scrollableContainer` property is deprecated.
48
+ // Use `getScrollableContainer()` property instead.
49
+ scrollableContainer,
50
+ getScrollableContainer,
51
+ getColumnsCount,
52
+ getItemId,
53
+ tbody: AsComponent === 'tbody',
54
+ state: initialState,
55
+ onStateChange
56
+ }
57
+ )
58
+ }, [])
59
+ }
@@ -0,0 +1,12 @@
1
+ import { useLayoutEffect } from 'react'
2
+
3
+ export default function useVirtualScrollerStartStop(virtualScroller) {
4
+ useLayoutEffect(() => {
5
+ // Start listening to scroll events.
6
+ virtualScroller.start()
7
+ return () => {
8
+ // Stop listening to scroll events.
9
+ virtualScroller.stop()
10
+ }
11
+ }, [])
12
+ }
@@ -0,0 +1,11 @@
1
+ import ItemsContainer from './ItemsContainer.js'
2
+ import ScrollableContainer from './ScrollableContainer.js'
3
+
4
+ export default {
5
+ createItemsContainer(getItemsContainerElement) {
6
+ return new ItemsContainer(getItemsContainerElement)
7
+ },
8
+ createScrollableContainer(getScrollableContainerElement, getItemsContainerElement) {
9
+ return new ScrollableContainer(getScrollableContainerElement, getItemsContainerElement)
10
+ }
11
+ }
@@ -0,0 +1,87 @@
1
+ export default class ItemsContainer {
2
+ /**
3
+ * Constructs a new "container" from an element.
4
+ * @param {function} getElement
5
+ */
6
+ constructor(getElement) {
7
+ this.getElement = getElement
8
+ }
9
+
10
+ /**
11
+ * Returns an item element's "top offset", relative to the items `container`'s top edge.
12
+ * @param {number} renderedElementIndex — An index of an item relative to the "first shown item index". For example, if the list is showing items from index 8 to index 12 then `renderedElementIndex = 0` would mean the item at index `8`.
13
+ * @return {number}
14
+ */
15
+ getNthRenderedItemTopOffset(renderedElementIndex) {
16
+ const children = this.getElement().children
17
+ const maxWidth = this.getElement().width
18
+ let topOffset = this.getElement().paddingTop
19
+ let rowWidth
20
+ let rowHeight
21
+ let startNewRow = true
22
+ let i = 0
23
+ while (i <= renderedElementIndex) {
24
+ if (startNewRow || rowWidth + children[i].width > maxWidth) {
25
+ if (i > 0) {
26
+ topOffset += rowHeight
27
+ topOffset += children[i].marginTop
28
+ }
29
+ rowWidth = children[i].width
30
+ rowHeight = children[i].height
31
+ if (rowWidth > maxWidth) {
32
+ startNewRow = true
33
+ } else {
34
+ startNewRow = false
35
+ }
36
+ } else {
37
+ rowWidth += children[i].width
38
+ rowHeight = Math.max(rowHeight, children[i].height)
39
+ }
40
+ i++
41
+ }
42
+ return topOffset
43
+ }
44
+
45
+ /**
46
+ * Returns an item element's height.
47
+ * @param {number} renderedElementIndex — An index of an item relative to the "first shown item index". For example, if the list is showing items from index 8 to index 12 then `renderedElementIndex = 0` would mean the item at index `8`.
48
+ * @return {number}
49
+ */
50
+ getNthRenderedItemHeight(renderedElementIndex) {
51
+ return this.getElement().children[renderedElementIndex].height
52
+ }
53
+
54
+ /**
55
+ * Returns items container height.
56
+ * @return {number}
57
+ */
58
+ getHeight() {
59
+ const children = this.getElement().children
60
+ const maxWidth = this.getElement().width
61
+ let contentHeight = this.getElement().paddingTop
62
+ let i = 0
63
+ while (i < children.length) {
64
+ let rowWidth = 0
65
+ let rowHeight = 0
66
+ while (rowWidth <= maxWidth && i < children.length) {
67
+ if (rowWidth === 0 && i > 0) {
68
+ const verticalSpacing = children[i].marginTop
69
+ contentHeight += verticalSpacing
70
+ }
71
+ rowWidth += children[i].width
72
+ rowHeight = Math.max(rowHeight, children[i].height)
73
+ i++
74
+ }
75
+ contentHeight += rowHeight
76
+ }
77
+ contentHeight += this.getElement().paddingBottom
78
+ return contentHeight
79
+ }
80
+
81
+ /**
82
+ * Removes all item elements of an items container.
83
+ */
84
+ clear() {
85
+ this.getElement().children = []
86
+ }
87
+ }
@@ -0,0 +1,88 @@
1
+ export default class ScrollableContainer {
2
+ /**
3
+ * Constructs a new "scrollable container" from an element.
4
+ * @param {func} getElement — Returns the scrollable container element.
5
+ * @param {func} getItemsContainerElement — Returns items "container" element.
6
+ */
7
+ constructor(getElement, getItemsContainerElement) {
8
+ this.getElement = getElement
9
+ this.getItemsContainerElement = getItemsContainerElement
10
+ this.scrollTop = 0
11
+ }
12
+
13
+ /**
14
+ * Returns the current scroll position.
15
+ * @return {number}
16
+ */
17
+ getScrollY() {
18
+ return this.scrollTop
19
+ }
20
+
21
+ /**
22
+ * Scrolls to a specific position.
23
+ * @param {number} scrollY
24
+ */
25
+ scrollToY(scrollY) {
26
+ this.scrollTop = scrollY
27
+ if (this.onScrollListener) {
28
+ this.onScrollListener()
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Returns "scrollable container" width,
34
+ * i.e. the available width for its content.
35
+ * @return {number}
36
+ */
37
+ getWidth() {
38
+ return this.getElement().width
39
+ }
40
+
41
+ /**
42
+ * Returns the height of the "scrollable container" itself.
43
+ * Not to be confused with the height of "scrollable container"'s content.
44
+ * @return {number}
45
+ */
46
+ getHeight() {
47
+ return this.getElement().height
48
+ }
49
+
50
+ /**
51
+ * Returns a "top offset" of an items container element
52
+ * relative to the "scrollable container"'s top edge.
53
+ * @return {number}
54
+ */
55
+ getItemsContainerTopOffset() {
56
+ return 0
57
+ }
58
+
59
+ /**
60
+ * Adds a "scroll" event listener to the "scrollable container".
61
+ * @param {onScroll} Should be called whenever the scroll position inside the "scrollable container" (potentially) changes.
62
+ * @return {function} Returns a function that stops listening.
63
+ */
64
+ onScroll(onScroll) {
65
+ this.onScrollListener = onScroll
66
+ return () => {
67
+ delete this.onScrollListener
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Adds a "resize" event listener to the "scrollable container".
73
+ * @param {onResize} Should be called whenever the "scrollable container"'s width or height (potentially) changes.
74
+ * @return {function} Returns a function that stops listening.
75
+ */
76
+ onResize(onResize) {
77
+ this.onResizeListener = onResize
78
+ return () => {
79
+ delete this.onResizeListener
80
+ }
81
+ }
82
+
83
+ // Returns a `Promise` because `this.onResizeListener()` is a "debounced" function.
84
+ // See `./utility/debounce.js` for more details.
85
+ _triggerResizeListener() {
86
+ return this.onResizeListener()
87
+ }
88
+ }