@octane-xplat/ui 0.7.2 → 0.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.
- package/README.md +4 -3
- package/dist/native/Button.tsrx.js +1 -1
- package/dist/native/CommandPalette.tsrx.js +7 -1
- package/dist/native/Grid.tsrx.js +31 -2
- package/dist/native/KeyboardAvoiding.mobile.tsrx.js +2 -2
- package/dist/native/Markdown.tsrx.js +233 -0
- package/dist/native/Overlay.tsrx.js +15 -26
- package/dist/native/Pagination.tsrx.js +15 -12
- package/dist/native/PinInput.tsrx.js +25 -7
- package/dist/native/Popover.tsrx.js +33 -23
- package/dist/native/Pressable.tsrx.js +4 -1
- package/dist/native/Screen.tsrx.js +1 -1
- package/dist/native/SearchInput.tsrx.js +6 -3
- package/dist/native/Sheet.tsrx.js +23 -38
- package/dist/native/Slider.tsrx.js +23 -14
- package/dist/native/Text.tsrx.js +16 -0
- package/dist/native/TextArea.tsrx.js +6 -3
- package/dist/native/TextInput.tsrx.js +6 -3
- package/dist/native/VirtualList.tsrx.js +305 -121
- package/dist/native/WebView.tsrx.js +17 -2
- package/dist/native/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js +9 -0
- package/dist/native/escape-props.js +2 -0
- package/dist/native/grid-auto-placement.js +83 -0
- package/dist/native/grid-placement.js +20 -0
- package/dist/native/index.shared.js +7 -5
- package/dist/native/keyboard-inset.mobile.js +5 -5
- package/dist/native/navigation-request.js +26 -0
- package/dist/native/octane-client-build.json +1 -1
- package/dist/native/overlay-lifetime.mobile.js +65 -0
- package/dist/native/pan.tsrx.js +16 -2
- package/dist/native/route-table.js +90 -10
- package/dist/native/route.js +26 -16
- package/dist/native/shared.js +7 -5
- package/dist/native/sheet-service.js +2 -2
- package/dist/native/svg.mobile.js +1 -1
- package/dist/native/{tap-to-blur.mobile.js → tap-to-blur.ios.js} +20 -32
- package/dist/native/text-focus.js +10 -0
- package/dist/native/text-write.js +18 -11
- package/dist/native/toast-anchor.tsrx.js +1 -1
- package/dist/native/ui.js +7 -6
- package/dist/native/useMeasure.tsrx.js +17 -18
- package/dist/native/vendor/ui-svg/index.android.js +2 -2
- package/dist/native/vendor/ui-svg/index.ios.js +2 -2
- package/dist/native/vendor/ui-svg/{index.common.js → src/index.common.js} +1 -1
- package/dist/native/virtual-list-cell-pool.js +63 -0
- package/dist/web/Button.web.tsrx.js +1 -1
- package/dist/web/KeyboardAvoiding.web.tsrx.js +78 -0
- package/dist/web/Markdown.tsrx.js +98 -0
- package/dist/web/Overlay.web.tsrx.js +81 -32
- package/dist/web/PinInput.web.tsrx.js +24 -10
- package/dist/web/Pressable.web.tsrx.js +169 -53
- package/dist/web/SearchInput.web.tsrx.js +2 -2
- package/dist/web/Sheet.web.tsrx.js +20 -7
- package/dist/web/TextArea.web.tsrx.js +22 -17
- package/dist/web/TextInput.web.tsrx.js +2 -2
- package/dist/web/VirtualList.web.tsrx.js +220 -109
- package/dist/web/WebView.web.tsrx.js +3 -3
- package/dist/web/{index.shared.js → index.shared.web.js} +7 -5
- package/dist/web/modal-focus.web.js +95 -0
- package/dist/web/navigation-request.js +26 -0
- package/dist/web/octane-client-build.json +1 -1
- package/dist/web/pan.web.tsrx.js +27 -10
- package/dist/web/route-table.js +90 -10
- package/dist/web/route.web.js +88 -22
- package/dist/web/sheet-service.web.js +8 -0
- package/dist/web/theme/colorScheme.web.js +31 -4
- package/dist/web/ui.js +8 -6
- package/dist/web/virtual-list-cell-pool.js +63 -0
- package/package.json +21 -19
- package/platforms/android/native-api-usage.json +7 -7
- package/src/CommandPalette.tsrx +8 -1
- package/src/Grid.tsrx +32 -1
- package/src/Icon.macos.tsrx +1 -0
- package/src/KeyboardAvoiding.macos.tsrx +7 -1
- package/src/KeyboardAvoiding.mobile.tsrx +2 -2
- package/src/KeyboardAvoiding.tsrx +15 -0
- package/src/KeyboardAvoiding.web.tsrx +15 -0
- package/src/Markdown.tsrx +121 -0
- package/src/Meter.tsrx +1 -0
- package/src/Overlay.tsrx +15 -30
- package/src/Overlay.web.tsrx +12 -3
- package/src/Pagination.tsrx +6 -4
- package/src/PinInput.macos.tsrx +20 -6
- package/src/PinInput.mobile.test.ts +107 -0
- package/src/PinInput.tsrx +20 -6
- package/src/PinInput.web.test.tsrx +42 -0
- package/src/PinInput.web.tsrx +20 -6
- package/src/Popover.tsrx +36 -22
- package/src/Pressable.tsrx +1 -1
- package/src/Pressable.web.test.tsrx +36 -0
- package/src/Pressable.web.tsrx +37 -6
- package/src/RouteHost.macos.tsrx +1 -0
- package/src/Screen.tsrx +1 -1
- package/src/SearchInput.tsrx +4 -3
- package/src/SearchInput.web.tsrx +1 -1
- package/src/Sheet.tsrx +21 -42
- package/src/Sheet.web.tsrx +9 -1
- package/src/Slider.tsrx +13 -11
- package/src/Text.tsrx +36 -0
- package/src/TextArea.tsrx +6 -5
- package/src/TextArea.web.test.tsrx +39 -0
- package/src/TextArea.web.tsrx +7 -5
- package/src/TextInput.tsrx +6 -6
- package/src/TextInput.web.tsrx +1 -1
- package/src/VirtualList.macos.tsrx +93 -6
- package/src/VirtualList.mobile.test.tsrx +232 -0
- package/src/VirtualList.tsrx +285 -81
- package/src/VirtualList.web.tsrx +131 -25
- package/src/WebView.tsrx +20 -10
- package/src/WebView.web.tsrx +3 -0
- package/src/android/BottomNavigationView.android.tsrx +1 -0
- package/src/android/MaterialDialog.android.tsrx +1 -0
- package/src/anim.macos.tsrx +1 -0
- package/src/apply-native-modifiers.native.ts +4 -1
- package/src/child-array.ts +12 -7
- package/src/colorScheme.macos.ts +1 -0
- package/src/controls.macos.tsrx +4 -1
- package/src/data-driver.mobile.test.ts +120 -0
- package/src/data-lifecycle.mobile.test.ts +251 -0
- package/src/escape-props.ts +13 -1
- package/src/grid-auto-placement.ts +163 -0
- package/src/grid-placement.ts +29 -0
- package/src/hosted-unsupported.macos.tsrx +1 -0
- package/src/index.macos.ts +23 -3
- package/src/index.mobile.ts +0 -1
- package/src/index.shared.ts +22 -1
- package/src/index.shared.web.ts +291 -0
- package/src/index.web.ts +1 -1
- package/src/input-readiness.web.test.tsrx +82 -0
- package/src/ios/UITabBar.ios.tsrx +1 -0
- package/src/keyboard-inset.mobile.test.ts +62 -0
- package/src/keyboard-inset.mobile.ts +63 -43
- package/src/layout-shells.macos.tsrx +1 -5
- package/src/macos-extras.macos.tsrx +2 -0
- package/src/modal-focus.web.test.tsrx +63 -0
- package/src/modal-focus.web.ts +162 -0
- package/src/navigation-request.ts +37 -0
- package/src/overlay-lifecycle.mobile.test.ts +316 -0
- package/src/overlay-lifetime.mobile.ts +107 -0
- package/src/pan.mobile.test.tsrx +116 -0
- package/src/pan.tsrx +20 -2
- package/src/pan.web.tsrx +35 -7
- package/src/platformProps.ts +1 -0
- package/src/popover.mobile.test.ts +9 -1
- package/src/pressable-accessibility.mobile.test.ts +30 -0
- package/src/props.ts +163 -3
- package/src/refresh.ts +6 -1
- package/src/refresh.web.test.ts +77 -77
- package/src/route-android.test.ts +162 -0
- package/src/route-history.web.test.ts +80 -0
- package/src/route-navigation.web.test.ts +173 -0
- package/src/route-table.test.ts +188 -9
- package/src/route-table.ts +138 -10
- package/src/route.macos.ts +56 -13
- package/src/route.ts +54 -24
- package/src/route.web.ts +141 -24
- package/src/sheet-detents.ts +5 -3
- package/src/sheet-service.macos.ts +5 -1
- package/src/sheet-service.ts +1 -1
- package/src/sheet-service.web.ts +8 -0
- package/src/sheet-snap.test.ts +23 -23
- package/src/sheet-snap.ts +6 -1
- package/src/svg.mobile.ts +9 -3
- package/src/tap-to-blur.ios.ts +106 -0
- package/src/tap-to-blur.mobile.ts +21 -95
- package/src/text-focus.ts +12 -0
- package/src/text-input.mobile.test.ts +89 -0
- package/src/text-write.ts +21 -11
- package/src/theme/colorScheme.linux.ts +2 -4
- package/src/theme/colorScheme.web.ts +51 -4
- package/src/theme/theme-scheme.macos.ts +8 -2
- package/src/theme/tokens.css +1740 -1718
- package/src/translate.macos.ts +4 -1
- package/src/useMeasure.tsrx +20 -21
- package/src/virtual-list-cell-pool.test.ts +53 -0
- package/src/virtual-list-cell-pool.ts +81 -0
- package/src/virtual-list-layout.test.ts +11 -8
- package/src/virtual-list-layout.ts +4 -8
- package/src/windows.linux.ts +7 -2
- package/src/windows.macos.ts +1 -0
- package/types/generated/.tsrx-typegen-manifest.json +16 -3
- package/types/generated/Grid.d.ts +0 -1
- package/types/generated/KeyboardAvoiding.d.ts +3 -0
- package/types/generated/KeyboardAvoiding.web.d.ts +2 -0
- package/types/generated/Markdown.d.ts +59 -0
- package/types/generated/TextArea.d.ts +2 -2
- package/types/generated/TextArea.web.d.ts +1 -1
- package/types/generated/TextInput.d.ts +2 -3
- package/types/generated/VirtualList.d.ts +3 -3
- package/types/generated/escape-props.d.ts +4 -0
- package/types/generated/grid-auto-placement.d.ts +13 -0
- package/types/generated/grid-placement.d.ts +5 -0
- package/types/generated/index.mobile.d.ts +0 -1
- package/types/generated/index.shared.d.ts +6 -3
- package/types/generated/index.shared.web.d.ts +114 -0
- package/types/generated/index.web.d.ts +1 -1
- package/types/generated/modal-focus.web.d.ts +3 -0
- package/types/generated/navigation-request.d.ts +10 -0
- package/types/generated/overlay-lifetime.mobile.d.ts +10 -0
- package/types/generated/props.d.ts +111 -3
- package/types/generated/route-table.d.ts +14 -3
- package/types/generated/svg.mobile.d.ts +2 -2
- package/types/generated/tap-to-blur.ios.d.ts +13 -0
- package/types/generated/tap-to-blur.mobile.d.ts +1 -15
- package/types/generated/text-focus.d.ts +3 -0
- package/types/generated/text-write.d.ts +3 -1
- package/types/generated/theme/colorScheme.web.d.ts +5 -0
- package/types/generated/virtual-list-cell-pool.d.ts +18 -0
- package/types/index.macos.d.ts +15 -3
- /package/src/vendor/ui-svg/{index.android.ts → src/index.android.ts} +0 -0
- /package/src/vendor/ui-svg/{index.common.ts → src/index.common.ts} +0 -0
- /package/src/vendor/ui-svg/{index.ios.ts → src/index.ios.ts} +0 -0
- /package/src/vendor/ui-svg/{index.ts → src/index.ts} +0 -0
- /package/src/vendor/ui-svg/{typings → src/typings}/SVGKit.d.ts +0 -0
- /package/src/vendor/ui-svg/{typings → src/typings}/android.d.ts +0 -0
- /package/src/vendor/ui-svg/{typings → src/typings}/ios.d.ts +0 -0
- /package/types/generated/vendor/ui-svg/{index.common.d.ts → src/index.common.d.ts} +0 -0
- /package/types/generated/vendor/ui-svg/{index.d.ts → src/index.d.ts} +0 -0
- /package/types/generated/vendor/ui-svg/{index.ios.d.ts → src/index.ios.d.ts} +0 -0
package/src/VirtualList.tsrx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @jsxImportSource @nativescript-community/octane */
|
|
2
|
-
import { useMemo, useRef, useState } from 'octane'
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'octane'
|
|
3
3
|
import type { VirtualListProps } from './props'
|
|
4
4
|
import {
|
|
5
5
|
createVirtualListEntries,
|
|
@@ -7,9 +7,14 @@ import {
|
|
|
7
7
|
virtualListMeasurementKey,
|
|
8
8
|
virtualListRange,
|
|
9
9
|
VirtualListSizeIndex,
|
|
10
|
+
VIRTUAL_LIST_ESTIMATED_ROW_SIZE,
|
|
11
|
+
VIRTUAL_LIST_MIN_TYPE_SAMPLES,
|
|
10
12
|
type VirtualListEntry,
|
|
11
13
|
type VirtualListSizeRecord,
|
|
12
14
|
} from './virtual-list-layout'
|
|
15
|
+
|
|
16
|
+
import { VirtualListCellPool, type VirtualListCell } from './virtual-list-cell-pool'
|
|
17
|
+
|
|
13
18
|
import { cx } from './cx'
|
|
14
19
|
import { applyEscapeProps, nativeAccessibilityRole, nativeAccessibilityState } from './escape-props'
|
|
15
20
|
import { layoutChildProps } from './layout-child'
|
|
@@ -18,40 +23,84 @@ function sameSize(a: number, b: number) {
|
|
|
18
23
|
return Math.abs(a - b) < 0.5
|
|
19
24
|
}
|
|
20
25
|
|
|
26
|
+
function scheduleListFrame(callback: () => void): () => void {
|
|
27
|
+
const requestFrame = (globalThis as any).requestAnimationFrame
|
|
28
|
+
const cancelFrame = (globalThis as any).cancelAnimationFrame
|
|
29
|
+
if (typeof requestFrame !== 'function' || typeof cancelFrame !== 'function') {
|
|
30
|
+
const timer = setTimeout(callback, 16)
|
|
31
|
+
return () => clearTimeout(timer)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let cancelled = false
|
|
35
|
+
let current: number | null = null
|
|
36
|
+
let next: number | null = null
|
|
37
|
+
// NativeScript dispatches an initial rAF through a macrotask. A nested
|
|
38
|
+
// request joins Choreographer/CADisplayLink rather than a free-running timer.
|
|
39
|
+
current = requestFrame(() => {
|
|
40
|
+
current = null
|
|
41
|
+
if (cancelled) return
|
|
42
|
+
next = requestFrame(() => {
|
|
43
|
+
next = null
|
|
44
|
+
if (!cancelled) callback()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
cancelled = true
|
|
50
|
+
if (current != null) cancelFrame(current)
|
|
51
|
+
if (next != null) cancelFrame(next)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
21
55
|
interface VirtualListAnchor {
|
|
22
56
|
rowKey: string
|
|
23
57
|
index: number
|
|
24
58
|
within: number
|
|
25
59
|
}
|
|
26
60
|
|
|
27
|
-
/** Shared vertical virtualization
|
|
28
|
-
* rows unmount; measured
|
|
29
|
-
* the
|
|
61
|
+
/** Shared vertical virtualization with pooled outer hosts and keyed logical
|
|
62
|
+
* rows. Off-window item subtrees unmount; measured heights position cells and
|
|
63
|
+
* preserve the visible keyed anchor after layout. */
|
|
30
64
|
export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
31
65
|
const entries = useMemo(
|
|
32
66
|
() => createVirtualListEntries(props.items, props.keyExtractor, props.getItemType),
|
|
33
67
|
[props.items, props.keyExtractor, props.getItemType],
|
|
34
68
|
)
|
|
69
|
+
|
|
35
70
|
const [windowVersion, setWindowVersion] = useState(0)
|
|
36
71
|
const sizeIndexRef = useRef(new VirtualListSizeIndex())
|
|
72
|
+
const cellsRef = useRef(new VirtualListCellPool<T>())
|
|
73
|
+
const cellNodesRef = useRef(new Map<number, any>())
|
|
74
|
+
const cellCallbacksRef = useRef(new Map<number, (view: any) => void>())
|
|
75
|
+
const measuredAssignmentsRef = useRef(new Map<number, number>())
|
|
37
76
|
const sizeCacheRef = useRef(new Map<string, VirtualListSizeRecord>())
|
|
77
|
+
const typeSamplesRef = useRef(new Map<string, { sum: number; count: number }>())
|
|
78
|
+
const appliedEstimatesRef = useRef(new Map<string, number>())
|
|
79
|
+
const estimateDirtyTypesRef = useRef(new Set<string>())
|
|
38
80
|
const entriesRef = useRef<VirtualListEntry<T>[]>([])
|
|
39
81
|
const indexByRowKeyRef = useRef(new Map<string, number>())
|
|
40
82
|
const scrollRef = useRef<any>(null)
|
|
41
83
|
const scrollOffsetRef = useRef(0)
|
|
42
84
|
const viewportHeightRef = useRef(0)
|
|
43
85
|
const viewportWidthRef = useRef(0)
|
|
86
|
+
const contentWidthRef = useRef(0)
|
|
44
87
|
const headerHeightRef = useRef(0)
|
|
45
|
-
const
|
|
46
|
-
const
|
|
88
|
+
const estimateTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
89
|
+
const frameCancelRef = useRef<(() => void) | null>(null)
|
|
90
|
+
const disposedRef = useRef(false)
|
|
91
|
+
const pendingOffsetDeltaRef = useRef(0)
|
|
92
|
+
const measurementsRef = useRef(
|
|
93
|
+
new Map<string, { entry: VirtualListEntry<T>; width: number; height: number; cellId: number; generation: number; view?: any }>(),
|
|
94
|
+
)
|
|
95
|
+
|
|
47
96
|
const rangeRef = useRef({ start: -1, end: -1, top: -1, bottom: -1 })
|
|
48
97
|
const pendingAnchorRef = useRef<VirtualListAnchor | null>(null)
|
|
49
98
|
const pendingAnchorRowsRef = useRef(new Set<string>())
|
|
50
|
-
const queuedAnchorRestoreRef = useRef<VirtualListAnchor | null>(null)
|
|
51
99
|
const anchorRestoreScheduledRef = useRef(false)
|
|
52
100
|
const anchorRestoreTimerRef = useRef<any>(null)
|
|
53
101
|
const hasSeparator = Boolean(props.renderSeparator)
|
|
54
102
|
const hasSeparatorRef = useRef(hasSeparator)
|
|
103
|
+
const separatorChanged = hasSeparatorRef.current !== hasSeparator
|
|
55
104
|
hasSeparatorRef.current = hasSeparator
|
|
56
105
|
|
|
57
106
|
function rebuildLayout(nextEntries: VirtualListEntry<T>[], width: number) {
|
|
@@ -65,6 +114,7 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
65
114
|
entry.rowKey,
|
|
66
115
|
hasSeparatorRef.current && entry.index < nextEntries.length - 1,
|
|
67
116
|
)
|
|
117
|
+
|
|
68
118
|
activeMeasurements.add(cacheKey)
|
|
69
119
|
}
|
|
70
120
|
|
|
@@ -72,13 +122,35 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
72
122
|
if (!activeMeasurements.has(cacheKey)) sizeCacheRef.current.delete(cacheKey)
|
|
73
123
|
}
|
|
74
124
|
|
|
125
|
+
const samples = new Map<string, { sum: number; count: number }>()
|
|
126
|
+
for (const entry of nextEntries) {
|
|
127
|
+
const cached = sizeCacheRef.current.get(virtualListMeasurementKey(
|
|
128
|
+
entry.rowKey, hasSeparatorRef.current && entry.index < nextEntries.length - 1,
|
|
129
|
+
))
|
|
130
|
+
|
|
131
|
+
if (!cached) continue
|
|
132
|
+
const sample = samples.get(entry.typeKey) ?? { sum: 0, count: 0 }
|
|
133
|
+
sample.sum += cached.height
|
|
134
|
+
sample.count++
|
|
135
|
+
samples.set(entry.typeKey, sample)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
typeSamplesRef.current = samples
|
|
139
|
+
estimateDirtyTypesRef.current.clear()
|
|
140
|
+
appliedEstimatesRef.current = new Map([...samples].map(([type, sample]) => [
|
|
141
|
+
type, sample.count >= VIRTUAL_LIST_MIN_TYPE_SAMPLES
|
|
142
|
+
? sample.sum / sample.count : VIRTUAL_LIST_ESTIMATED_ROW_SIZE,
|
|
143
|
+
]))
|
|
144
|
+
|
|
75
145
|
indexByRowKeyRef.current = indexByKey
|
|
76
146
|
sizeIndexRef.current.reset(
|
|
77
|
-
estimateVirtualListSizes(
|
|
147
|
+
estimateVirtualListSizes(
|
|
148
|
+
nextEntries, hasSeparatorRef.current, contentWidthRef.current || width, sizeCacheRef.current,
|
|
149
|
+
),
|
|
78
150
|
)
|
|
79
151
|
}
|
|
80
152
|
|
|
81
|
-
if (entries !== entriesRef.current) {
|
|
153
|
+
if (entries !== entriesRef.current || separatorChanged) {
|
|
82
154
|
const anchor = captureCurrentAnchor()
|
|
83
155
|
const previousByKey = new Map(entriesRef.current.map((entry) => [entry.rowKey, entry]))
|
|
84
156
|
const changedRows = entries.filter((entry) => previousByKey.get(entry.rowKey)?.item !== entry.item)
|
|
@@ -96,6 +168,7 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
96
168
|
)
|
|
97
169
|
.map((entry) => entry.rowKey),
|
|
98
170
|
)
|
|
171
|
+
|
|
99
172
|
pendingAnchorRef.current = pendingRows.size > 0 ? anchor : null
|
|
100
173
|
pendingAnchorRowsRef.current = pendingRows
|
|
101
174
|
|
|
@@ -112,7 +185,7 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
112
185
|
)
|
|
113
186
|
}
|
|
114
187
|
|
|
115
|
-
function
|
|
188
|
+
function commitWindow() {
|
|
116
189
|
const next = currentRange()
|
|
117
190
|
const previous = rangeRef.current
|
|
118
191
|
if (
|
|
@@ -126,6 +199,34 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
126
199
|
}
|
|
127
200
|
}
|
|
128
201
|
|
|
202
|
+
function refreshWindow() {
|
|
203
|
+
if (disposedRef.current || frameCancelRef.current != null) return
|
|
204
|
+
frameCancelRef.current = scheduleListFrame(() => {
|
|
205
|
+
frameCancelRef.current = null
|
|
206
|
+
if (disposedRef.current) return
|
|
207
|
+
const measurements = [...measurementsRef.current.values()]
|
|
208
|
+
measurementsRef.current.clear()
|
|
209
|
+
for (const measurement of measurements) {
|
|
210
|
+
if (!cellsRef.current.isCurrent(measurement.cellId, measurement.entry.rowKey, measurement.generation)) continue
|
|
211
|
+
const index = indexByRowKeyRef.current.get(measurement.entry.rowKey)
|
|
212
|
+
if (index === undefined || entriesRef.current[index]?.item !== measurement.entry.item ||
|
|
213
|
+
entriesRef.current[index]?.index !== measurement.entry.index) continue
|
|
214
|
+
|
|
215
|
+
if (measurement.view?.isLoaded === false) continue
|
|
216
|
+
if (measurement.view?.isLayoutValid === false) {
|
|
217
|
+
measurementsRef.current.set(measurement.entry.rowKey, measurement)
|
|
218
|
+
refreshWindow()
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const size = measurement.view?.getActualSize?.() ?? measurement
|
|
223
|
+
if (size.width > 0 && size.height >= 0) recordRowSize(measurement.entry.rowKey, size.width, size.height)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
commitWindow()
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
129
230
|
function captureAnchor(): VirtualListAnchor | null {
|
|
130
231
|
const listOffset = scrollOffsetRef.current - headerHeightRef.current
|
|
131
232
|
if (listOffset < 0 || entriesRef.current.length === 0) return null
|
|
@@ -142,50 +243,44 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
142
243
|
|
|
143
244
|
function captureCurrentAnchor() {
|
|
144
245
|
const nativeOffset = Number(scrollRef.current?.verticalOffset)
|
|
145
|
-
if (Number.isFinite(nativeOffset)) scrollOffsetRef.current = nativeOffset
|
|
246
|
+
if (Number.isFinite(nativeOffset)) scrollOffsetRef.current = nativeOffset + pendingOffsetDeltaRef.current
|
|
146
247
|
return captureAnchor()
|
|
147
248
|
}
|
|
148
249
|
|
|
149
250
|
function restoreAnchor(anchor: VirtualListAnchor | null) {
|
|
150
|
-
if (!anchor) return
|
|
151
|
-
|
|
152
|
-
if (anchorRestoreScheduledRef.current) return
|
|
251
|
+
if (!anchor || disposedRef.current) return
|
|
252
|
+
const index = indexByRowKeyRef.current.get(anchor.rowKey)
|
|
153
253
|
const view = scrollRef.current
|
|
154
|
-
if (!view) return
|
|
155
|
-
|
|
254
|
+
if (index === undefined || !view) return
|
|
255
|
+
const nextOffset = Math.max(0, headerHeightRef.current + sizeIndexRef.current.prefix(index) + anchor.within)
|
|
256
|
+
const delta = nextOffset - scrollOffsetRef.current
|
|
257
|
+
if (sameSize(delta, 0)) return
|
|
258
|
+
// Apply geometry corrections to the live offset, preserving intervening input.
|
|
259
|
+
pendingOffsetDeltaRef.current += delta
|
|
260
|
+
scrollOffsetRef.current = nextOffset
|
|
261
|
+
if (anchorRestoreScheduledRef.current) return
|
|
156
262
|
anchorRestoreScheduledRef.current = true
|
|
157
263
|
const timer = setTimeout(() => {
|
|
158
264
|
if (anchorRestoreTimerRef.current !== timer) return
|
|
159
265
|
anchorRestoreTimerRef.current = null
|
|
160
266
|
anchorRestoreScheduledRef.current = false
|
|
161
|
-
if (scrollRef.current !== view) return
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
queuedAnchorRestoreRef.current = null
|
|
165
|
-
if (!queuedAnchor) return
|
|
166
|
-
const index = indexByRowKeyRef.current.get(queuedAnchor.rowKey)
|
|
167
|
-
if (index === undefined) return
|
|
168
|
-
|
|
169
|
-
const nextOffset = Math.max(
|
|
170
|
-
0,
|
|
171
|
-
headerHeightRef.current + sizeIndexRef.current.prefix(index) + queuedAnchor.within,
|
|
172
|
-
)
|
|
267
|
+
if (disposedRef.current || scrollRef.current !== view) return
|
|
268
|
+
const correction = pendingOffsetDeltaRef.current
|
|
269
|
+
pendingOffsetDeltaRef.current = 0
|
|
173
270
|
const nativeOffset = Number(view.verticalOffset)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
scrollOffsetRef.current = nextOffset
|
|
271
|
+
const target = Math.max(0, nativeOffset + correction)
|
|
272
|
+
scrollOffsetRef.current = target
|
|
273
|
+
if (sameSize(target, nativeOffset)) return
|
|
180
274
|
const nativeView = view.nativeViewProtected
|
|
181
275
|
const makePoint = (globalThis as any).CGPointMake
|
|
182
276
|
if (typeof nativeView?.setContentOffsetAnimated === 'function' && typeof makePoint === 'function') {
|
|
183
277
|
const current = nativeView.contentOffset
|
|
184
|
-
nativeView.setContentOffsetAnimated(makePoint(current.x,
|
|
278
|
+
nativeView.setContentOffsetAnimated(makePoint(current.x, target), false)
|
|
185
279
|
} else if (typeof view.scrollToVerticalOffset === 'function') {
|
|
186
|
-
view.scrollToVerticalOffset(
|
|
280
|
+
view.scrollToVerticalOffset(target, false)
|
|
187
281
|
}
|
|
188
282
|
}, 0)
|
|
283
|
+
|
|
189
284
|
anchorRestoreTimerRef.current = timer
|
|
190
285
|
}
|
|
191
286
|
|
|
@@ -193,21 +288,19 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
193
288
|
if (anchorRestoreTimerRef.current != null) clearTimeout(anchorRestoreTimerRef.current)
|
|
194
289
|
anchorRestoreTimerRef.current = null
|
|
195
290
|
anchorRestoreScheduledRef.current = false
|
|
196
|
-
|
|
291
|
+
pendingOffsetDeltaRef.current = 0
|
|
197
292
|
}
|
|
198
293
|
|
|
199
|
-
function scheduleEstimateRebuild(
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const scheduledAnchor = estimateAnchorRef.current
|
|
206
|
-
estimateAnchorRef.current = null
|
|
294
|
+
function scheduleEstimateRebuild() {
|
|
295
|
+
if (estimateTimerRef.current != null) clearTimeout(estimateTimerRef.current)
|
|
296
|
+
estimateTimerRef.current = setTimeout(() => {
|
|
297
|
+
estimateTimerRef.current = null
|
|
298
|
+
if (disposedRef.current || estimateDirtyTypesRef.current.size === 0) return
|
|
299
|
+
const anchor = captureCurrentAnchor()
|
|
207
300
|
rebuildLayout(entriesRef.current, viewportWidthRef.current)
|
|
208
|
-
restoreAnchor(
|
|
301
|
+
restoreAnchor(anchor)
|
|
209
302
|
refreshWindow()
|
|
210
|
-
})
|
|
303
|
+
}, 120)
|
|
211
304
|
}
|
|
212
305
|
|
|
213
306
|
function recordRowSize(rowKey: string, width: number, height: number) {
|
|
@@ -220,14 +313,40 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
220
313
|
const hasRowSeparator = hasSeparatorRef.current && index < entriesRef.current.length - 1
|
|
221
314
|
const cacheKey = virtualListMeasurementKey(rowKey, hasRowSeparator)
|
|
222
315
|
const pendingAnchor = pendingAnchorRowsRef.current.has(rowKey) ? pendingAnchorRef.current : null
|
|
223
|
-
const
|
|
316
|
+
const previous = sizeCacheRef.current.get(cacheKey)
|
|
317
|
+
if (previous && sameSize(previous.width, width) && sameSize(previous.height, height) && sameSize(sizeIndexRef.current.get(index), height)) {
|
|
318
|
+
pendingAnchorRowsRef.current.delete(rowKey)
|
|
319
|
+
if (pendingAnchorRowsRef.current.size === 0) pendingAnchorRef.current = null
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const anchor = pendingAnchor ?? captureCurrentAnchor()
|
|
224
324
|
sizeCacheRef.current.set(cacheKey, { width, height })
|
|
325
|
+
const sample = typeSamplesRef.current.get(entry.typeKey) ?? { sum: 0, count: 0 }
|
|
326
|
+
sample.sum += height - (previous?.height ?? 0)
|
|
327
|
+
if (!previous) sample.count++
|
|
328
|
+
typeSamplesRef.current.set(entry.typeKey, sample)
|
|
329
|
+
const applied = appliedEstimatesRef.current.get(entry.typeKey) ?? VIRTUAL_LIST_ESTIMATED_ROW_SIZE
|
|
330
|
+
const estimateChanged = sample.count >= VIRTUAL_LIST_MIN_TYPE_SAMPLES &&
|
|
331
|
+
Math.abs(sample.sum / sample.count - applied) > Math.max(1, applied * 0.02)
|
|
332
|
+
|
|
225
333
|
const delta = sizeIndexRef.current.set(index, height)
|
|
226
334
|
if (Math.abs(delta) >= 0.5) {
|
|
227
335
|
const anchorIndex = anchor ? indexByRowKeyRef.current.get(anchor.rowKey) : undefined
|
|
228
336
|
if (anchor && anchorIndex !== undefined && index < anchorIndex) restoreAnchor(anchor)
|
|
229
|
-
|
|
230
|
-
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Exact measurements always apply. Tiny changes to a type average do not
|
|
340
|
+
// justify rebuilding every unvisited estimate and relaying out the list.
|
|
341
|
+
if (estimateChanged) {
|
|
342
|
+
estimateDirtyTypesRef.current.add(entry.typeKey)
|
|
343
|
+
scheduleEstimateRebuild()
|
|
344
|
+
} else {
|
|
345
|
+
estimateDirtyTypesRef.current.delete(entry.typeKey)
|
|
346
|
+
if (estimateDirtyTypesRef.current.size === 0 && estimateTimerRef.current != null) {
|
|
347
|
+
clearTimeout(estimateTimerRef.current)
|
|
348
|
+
estimateTimerRef.current = null
|
|
349
|
+
}
|
|
231
350
|
}
|
|
232
351
|
|
|
233
352
|
if (pendingAnchor) {
|
|
@@ -241,50 +360,129 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
241
360
|
const size = view.getActualSize?.()
|
|
242
361
|
if (!size) return
|
|
243
362
|
|
|
363
|
+
const initialViewport = viewportHeightRef.current <= 0
|
|
244
364
|
const widthChanged = !sameSize(size.width, viewportWidthRef.current)
|
|
245
365
|
viewportHeightRef.current = size.height
|
|
246
366
|
if (widthChanged) {
|
|
247
|
-
const anchor =
|
|
367
|
+
const anchor = initialViewport ? null : captureCurrentAnchor()
|
|
248
368
|
viewportWidthRef.current = size.width
|
|
249
369
|
rebuildLayout(entriesRef.current, size.width)
|
|
250
370
|
restoreAnchor(anchor)
|
|
251
371
|
}
|
|
252
|
-
|
|
372
|
+
|
|
373
|
+
// Populate the first viewport before its initial native layout finishes.
|
|
374
|
+
if (initialViewport) commitWindow()
|
|
375
|
+
else refreshWindow()
|
|
253
376
|
}
|
|
254
377
|
|
|
255
378
|
function onScroll(event: any) {
|
|
256
379
|
const view = event.object ?? scrollRef.current
|
|
257
380
|
const nextOffset = Number(view?.verticalOffset ?? 0)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const anchorIndex = indexByRowKeyRef.current.get(queuedAnchor.rowKey)
|
|
261
|
-
const nextIndex = sizeIndexRef.current.indexAt(
|
|
262
|
-
Math.max(0, nextOffset - headerHeightRef.current),
|
|
263
|
-
)
|
|
264
|
-
if (anchorIndex !== undefined && Math.abs(nextIndex - anchorIndex) > 1) cancelAnchorRestore()
|
|
381
|
+
if (Math.abs(nextOffset + pendingOffsetDeltaRef.current - scrollOffsetRef.current) > viewportHeightRef.current) {
|
|
382
|
+
cancelAnchorRestore()
|
|
265
383
|
}
|
|
266
|
-
|
|
384
|
+
|
|
385
|
+
if (!sameSize(nextOffset + pendingOffsetDeltaRef.current, scrollOffsetRef.current)) {
|
|
386
|
+
pendingAnchorRef.current = null
|
|
387
|
+
pendingAnchorRowsRef.current.clear()
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
scrollOffsetRef.current = nextOffset + pendingOffsetDeltaRef.current
|
|
391
|
+
if (estimateTimerRef.current != null) scheduleEstimateRebuild()
|
|
392
|
+
refreshWindow()
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function onContentLayout(event: any) {
|
|
396
|
+
const width = Number(event.object?.getActualSize?.().width ?? 0)
|
|
397
|
+
if (width <= 0 || sameSize(width, contentWidthRef.current)) return
|
|
398
|
+
const anchor = scrollOffsetRef.current > 0 ? captureCurrentAnchor() : null
|
|
399
|
+
contentWidthRef.current = width
|
|
400
|
+
rebuildLayout(entriesRef.current, viewportWidthRef.current)
|
|
401
|
+
restoreAnchor(anchor)
|
|
267
402
|
refreshWindow()
|
|
268
403
|
}
|
|
269
404
|
|
|
270
405
|
function onHeaderLayout(event: any) {
|
|
271
406
|
const height = Number(event.object?.getActualSize?.().height ?? 0)
|
|
272
407
|
if (sameSize(height, headerHeightRef.current)) return
|
|
273
|
-
const anchor =
|
|
408
|
+
const anchor = scrollOffsetRef.current > 0 ? captureCurrentAnchor() : null
|
|
274
409
|
headerHeightRef.current = height
|
|
275
410
|
restoreAnchor(anchor)
|
|
276
411
|
refreshWindow()
|
|
277
412
|
}
|
|
278
413
|
|
|
279
|
-
function
|
|
414
|
+
function cellCallback(id: number) {
|
|
415
|
+
let callback = cellCallbacksRef.current.get(id)
|
|
416
|
+
if (!callback) {
|
|
417
|
+
callback = (view: any) => {
|
|
418
|
+
if (view) {
|
|
419
|
+
// Preserve native widgets when the renderer moves a keyed host.
|
|
420
|
+
view.reusable = true
|
|
421
|
+
cellNodesRef.current.set(id, view)
|
|
422
|
+
} else {
|
|
423
|
+
cellNodesRef.current.get(id)?.destroyNode?.(true)
|
|
424
|
+
cellNodesRef.current.delete(id)
|
|
425
|
+
cellCallbacksRef.current.delete(id)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
cellCallbacksRef.current.set(id, callback)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return callback
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function onRowLayout(cell: VirtualListCell<T>, event: any) {
|
|
436
|
+
const entry = cell.entry
|
|
280
437
|
const size = event.object?.getActualSize?.()
|
|
281
|
-
if (
|
|
438
|
+
if (!entry || !size || disposedRef.current) return
|
|
439
|
+
measurementsRef.current.set(entry.rowKey, { entry, width: size.width, height: size.height, cellId: cell.id, generation: cell.generation })
|
|
440
|
+
refreshWindow()
|
|
282
441
|
}
|
|
283
442
|
|
|
443
|
+
useEffect(() => {
|
|
444
|
+
disposedRef.current = false
|
|
445
|
+
return () => {
|
|
446
|
+
disposedRef.current = true
|
|
447
|
+
frameCancelRef.current?.()
|
|
448
|
+
if (estimateTimerRef.current != null) clearTimeout(estimateTimerRef.current)
|
|
449
|
+
frameCancelRef.current = null
|
|
450
|
+
estimateTimerRef.current = null
|
|
451
|
+
measurementsRef.current.clear()
|
|
452
|
+
cellsRef.current.clear()
|
|
453
|
+
cellNodesRef.current.clear()
|
|
454
|
+
cellCallbacksRef.current.clear()
|
|
455
|
+
measuredAssignmentsRef.current.clear()
|
|
456
|
+
cancelAnchorRestore()
|
|
457
|
+
}
|
|
458
|
+
}, [])
|
|
459
|
+
|
|
284
460
|
const range = currentRange()
|
|
285
461
|
rangeRef.current = range
|
|
286
462
|
void windowVersion
|
|
287
|
-
const
|
|
463
|
+
const cells = cellsRef.current.update(entries.slice(range.start, range.end)).map((cell) => ({
|
|
464
|
+
...cell, top: cell.entry ? sizeIndexRef.current.prefix(cell.entry.index) : 0,
|
|
465
|
+
}))
|
|
466
|
+
|
|
467
|
+
const contentHeight = sizeIndexRef.current.total
|
|
468
|
+
useEffect(() => {
|
|
469
|
+
for (const cell of cells) {
|
|
470
|
+
if (!cell.entry || measuredAssignmentsRef.current.get(cell.id) === cell.generation) continue
|
|
471
|
+
const view = cellNodesRef.current.get(cell.id)
|
|
472
|
+
if (!view) continue
|
|
473
|
+
measuredAssignmentsRef.current.set(cell.id, cell.generation)
|
|
474
|
+
measurementsRef.current.set(cell.entry.rowKey, {
|
|
475
|
+
entry: cell.entry, width: 0, height: 0, view, cellId: cell.id, generation: cell.generation,
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
refreshWindow()
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
for (const id of measuredAssignmentsRef.current.keys()) {
|
|
482
|
+
if (!cellNodesRef.current.has(id)) measuredAssignmentsRef.current.delete(id)
|
|
483
|
+
}
|
|
484
|
+
}, [cells])
|
|
485
|
+
|
|
288
486
|
const style = { ...props.style, minHeight: props.style?.minHeight ?? 0 }
|
|
289
487
|
|
|
290
488
|
<scrollview
|
|
@@ -308,7 +506,7 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
308
506
|
onLayoutChanged={(event: any) => syncViewport(event.object)}
|
|
309
507
|
onScroll={onScroll}
|
|
310
508
|
>
|
|
311
|
-
<flexboxlayout flexDirection="column" className={cx('vx-virtual-list-content')}>
|
|
509
|
+
<flexboxlayout flexDirection="column" className={cx('vx-virtual-list-content')} onLayoutChanged={onContentLayout}>
|
|
312
510
|
{props.renderHeader ? (
|
|
313
511
|
<flexboxlayout
|
|
314
512
|
flexDirection="column"
|
|
@@ -326,27 +524,33 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
326
524
|
</flexboxlayout>
|
|
327
525
|
) : null
|
|
328
526
|
) : (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
@for (const entry of visibleEntries; key entry.rowKey) {
|
|
527
|
+
<absolutelayout className="vx-virtual-list-body" style={{ height: contentHeight, flexShrink: 0 }}>
|
|
528
|
+
@for (const cell of cells; key cell.id) {
|
|
332
529
|
<flexboxlayout
|
|
530
|
+
ref={cellCallback(cell.id)}
|
|
333
531
|
flexDirection="column"
|
|
334
|
-
className=
|
|
335
|
-
|
|
336
|
-
|
|
532
|
+
className={cell.entry ? 'vx-virtual-list-cell vx-virtual-list-row' : 'vx-virtual-list-cell'}
|
|
533
|
+
visibility={cell.entry ? 'visible' : 'collapse'}
|
|
534
|
+
left={0}
|
|
535
|
+
top={cell.top}
|
|
536
|
+
width={contentWidthRef.current || viewportWidthRef.current || '100%'}
|
|
537
|
+
onLayoutChanged={(event: any) => onRowLayout(cell, event)}
|
|
337
538
|
>
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
{
|
|
344
|
-
|
|
345
|
-
|
|
539
|
+
@for (const entry of cell.rows; key entry.rowKey) {
|
|
540
|
+
<>
|
|
541
|
+
<flexboxlayout flexDirection="column" className="vx-virtual-list-item">
|
|
542
|
+
{props.renderItem(entry.item, entry.index)}
|
|
543
|
+
</flexboxlayout>
|
|
544
|
+
{hasSeparator && entry.index < entries.length - 1 ? (
|
|
545
|
+
<flexboxlayout flexDirection="column" className="vx-virtual-list-separator">
|
|
546
|
+
{props.renderSeparator?.()}
|
|
547
|
+
</flexboxlayout>
|
|
548
|
+
) : null}
|
|
549
|
+
</>
|
|
550
|
+
}
|
|
346
551
|
</flexboxlayout>
|
|
347
552
|
}
|
|
348
|
-
|
|
349
|
-
</>
|
|
553
|
+
</absolutelayout>
|
|
350
554
|
)}
|
|
351
555
|
{props.renderFooter ? (
|
|
352
556
|
<flexboxlayout flexDirection="column" className="vx-virtual-list-footer" flexShrink={0}>
|