@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.web.tsrx
CHANGED
|
@@ -9,12 +9,15 @@ import {
|
|
|
9
9
|
type VirtualListEntry,
|
|
10
10
|
type VirtualListSizeRecord,
|
|
11
11
|
} from './virtual-list-layout'
|
|
12
|
+
|
|
13
|
+
import { VirtualListCellPool } from './virtual-list-cell-pool'
|
|
14
|
+
|
|
12
15
|
import { applyEscapeProps } from './escape-props'
|
|
13
16
|
import { layoutChildProps } from './layout-child.web'
|
|
14
17
|
|
|
15
18
|
type NodeMeta =
|
|
16
19
|
| { kind: 'viewport' }
|
|
17
|
-
| { kind: 'row'; rowKey: string }
|
|
20
|
+
| { kind: 'row'; rowKey: string; cellId: number; generation: number }
|
|
18
21
|
| { kind: 'header' }
|
|
19
22
|
| { kind: 'footer' }
|
|
20
23
|
|
|
@@ -29,8 +32,13 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
29
32
|
() => createVirtualListEntries(props.items, props.keyExtractor, props.getItemType),
|
|
30
33
|
[props.items, props.keyExtractor, props.getItemType],
|
|
31
34
|
)
|
|
35
|
+
|
|
32
36
|
const [windowVersion, setWindowVersion] = useState(0)
|
|
33
37
|
const sizeIndexRef = useRef(new VirtualListSizeIndex())
|
|
38
|
+
const cellsRef = useRef(new VirtualListCellPool<T>())
|
|
39
|
+
const cellCallbacksRef = useRef(new Map<number, (node: Element | null) => void>())
|
|
40
|
+
const measuredAssignmentsRef = useRef(new Map<number, number>())
|
|
41
|
+
const disposedRef = useRef(false)
|
|
34
42
|
const sizeCacheRef = useRef(new Map<string, VirtualListSizeRecord>())
|
|
35
43
|
const entriesRef = useRef<VirtualListEntry<T>[]>([])
|
|
36
44
|
const indexByRowKeyRef = useRef(new Map<string, number>())
|
|
@@ -42,6 +50,8 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
42
50
|
const footerHeightRef = useRef(0)
|
|
43
51
|
const observerRef = useRef<ResizeObserver | null>(null)
|
|
44
52
|
const estimateScheduledRef = useRef(false)
|
|
53
|
+
const resizeDeliveryRef = useRef(false)
|
|
54
|
+
const refreshAfterResizeRef = useRef(false)
|
|
45
55
|
const nodesBySlotRef = useRef(new Map<string, Element>())
|
|
46
56
|
const metaByNodeRef = useRef(new WeakMap<Element, NodeMeta>())
|
|
47
57
|
const rangeRef = useRef({ start: -1, end: -1, top: -1, bottom: -1 })
|
|
@@ -61,6 +71,7 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
61
71
|
entry.rowKey,
|
|
62
72
|
hasSeparatorRef.current && entry.index < nextEntries.length - 1,
|
|
63
73
|
)
|
|
74
|
+
|
|
64
75
|
activeMeasurements.add(cacheKey)
|
|
65
76
|
}
|
|
66
77
|
|
|
@@ -74,7 +85,11 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
74
85
|
)
|
|
75
86
|
}
|
|
76
87
|
|
|
77
|
-
if (entries !== entriesRef.current)
|
|
88
|
+
if (entries !== entriesRef.current) {
|
|
89
|
+
const anchor = captureAnchor()
|
|
90
|
+
rebuildLayout(entries, viewportWidthRef.current)
|
|
91
|
+
restoreAnchor(anchor)
|
|
92
|
+
}
|
|
78
93
|
|
|
79
94
|
function currentRange() {
|
|
80
95
|
const listOffset = Math.max(0, scrollOffsetRef.current - headerHeightRef.current)
|
|
@@ -87,6 +102,12 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
function refreshWindow() {
|
|
105
|
+
if (disposedRef.current) return
|
|
106
|
+
if (resizeDeliveryRef.current) {
|
|
107
|
+
refreshAfterResizeRef.current = true
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
90
111
|
const next = currentRange()
|
|
91
112
|
const previous = rangeRef.current
|
|
92
113
|
if (
|
|
@@ -101,7 +122,8 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
101
122
|
}
|
|
102
123
|
|
|
103
124
|
function captureAnchor() {
|
|
104
|
-
const
|
|
125
|
+
const scrollOffset = scrollRef.current?.scrollTop ?? scrollOffsetRef.current
|
|
126
|
+
const listOffset = scrollOffset - headerHeightRef.current
|
|
105
127
|
if (listOffset < 0 || entriesRef.current.length === 0) return null
|
|
106
128
|
|
|
107
129
|
const index = sizeIndexRef.current.indexAt(listOffset)
|
|
@@ -122,6 +144,7 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
122
144
|
0,
|
|
123
145
|
headerHeightRef.current + sizeIndexRef.current.prefix(index) + anchor.within,
|
|
124
146
|
)
|
|
147
|
+
|
|
125
148
|
if (sameSize(nextOffset, scrollOffsetRef.current)) return
|
|
126
149
|
|
|
127
150
|
scrollOffsetRef.current = nextOffset
|
|
@@ -133,6 +156,7 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
133
156
|
estimateScheduledRef.current = true
|
|
134
157
|
Promise.resolve().then(() => {
|
|
135
158
|
estimateScheduledRef.current = false
|
|
159
|
+
if (disposedRef.current) return
|
|
136
160
|
const anchor = captureAnchor()
|
|
137
161
|
rebuildLayout(entriesRef.current, viewportWidthRef.current)
|
|
138
162
|
restoreAnchor(anchor)
|
|
@@ -141,7 +165,7 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
141
165
|
}
|
|
142
166
|
|
|
143
167
|
function recordRowSize(rowKey: string, width: number, height: number) {
|
|
144
|
-
if (!Number.isFinite(height) || height < 0) return
|
|
168
|
+
if (disposedRef.current || !Number.isFinite(height) || height < 0) return
|
|
145
169
|
const index = indexByRowKeyRef.current.get(rowKey)
|
|
146
170
|
if (index === undefined) return
|
|
147
171
|
const entry = entriesRef.current[index]
|
|
@@ -156,6 +180,7 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
156
180
|
if (anchor && index < anchor.index) restoreAnchor(anchor)
|
|
157
181
|
refreshWindow()
|
|
158
182
|
}
|
|
183
|
+
|
|
159
184
|
scheduleEstimateRebuild()
|
|
160
185
|
}
|
|
161
186
|
|
|
@@ -166,12 +191,14 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
166
191
|
metaByNodeRef.current.delete(previous)
|
|
167
192
|
nodesBySlotRef.current.delete(slot)
|
|
168
193
|
}
|
|
194
|
+
|
|
169
195
|
if (!node) {
|
|
170
196
|
if (previous) {
|
|
171
197
|
observerRef.current?.unobserve(previous)
|
|
172
198
|
metaByNodeRef.current.delete(previous)
|
|
173
199
|
nodesBySlotRef.current.delete(slot)
|
|
174
200
|
}
|
|
201
|
+
|
|
175
202
|
return
|
|
176
203
|
}
|
|
177
204
|
|
|
@@ -180,6 +207,24 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
180
207
|
observerRef.current?.observe(node)
|
|
181
208
|
}
|
|
182
209
|
|
|
210
|
+
function cellCallback(id: number) {
|
|
211
|
+
let callback = cellCallbacksRef.current.get(id)
|
|
212
|
+
if (!callback) {
|
|
213
|
+
callback = (node: Element | null) => {
|
|
214
|
+
const cell = cellsRef.current.get(id)
|
|
215
|
+
bindObservedNode('cell:' + id, node, {
|
|
216
|
+
kind: 'row', rowKey: cell?.entry?.rowKey ?? '', cellId: id, generation: cell?.generation ?? 0,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
if (!node) cellCallbacksRef.current.delete(id)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
cellCallbacksRef.current.set(id, callback)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return callback
|
|
226
|
+
}
|
|
227
|
+
|
|
183
228
|
handlersRef.current = (meta, entry) => {
|
|
184
229
|
if (meta.kind === 'viewport') {
|
|
185
230
|
const width = scrollRef.current?.clientWidth ?? entry.contentRect.width
|
|
@@ -192,13 +237,18 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
192
237
|
rebuildLayout(entriesRef.current, width)
|
|
193
238
|
restoreAnchor(anchor)
|
|
194
239
|
}
|
|
240
|
+
|
|
195
241
|
refreshWindow()
|
|
196
242
|
return
|
|
197
243
|
}
|
|
198
244
|
|
|
199
245
|
const height = entry.contentRect.height
|
|
200
246
|
if (meta.kind === 'row') {
|
|
201
|
-
|
|
247
|
+
if (!cellsRef.current.isCurrent(meta.cellId, meta.rowKey, meta.generation)) return
|
|
248
|
+
// A reused node may have an older ResizeObserver record. Read its
|
|
249
|
+
// current box after assignment rather than caching the former row.
|
|
250
|
+
const bounds = entry.target.getBoundingClientRect()
|
|
251
|
+
recordRowSize(meta.rowKey, bounds.width, bounds.height)
|
|
202
252
|
return
|
|
203
253
|
}
|
|
204
254
|
|
|
@@ -213,20 +263,50 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
213
263
|
}
|
|
214
264
|
}
|
|
215
265
|
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
disposedRef.current = false
|
|
268
|
+
return () => {
|
|
269
|
+
disposedRef.current = true
|
|
270
|
+
cellsRef.current.clear()
|
|
271
|
+
cellCallbacksRef.current.clear()
|
|
272
|
+
measuredAssignmentsRef.current.clear()
|
|
273
|
+
nodesBySlotRef.current.clear()
|
|
274
|
+
metaByNodeRef.current = new WeakMap()
|
|
275
|
+
}
|
|
276
|
+
}, [])
|
|
277
|
+
|
|
216
278
|
useEffect(() => {
|
|
217
279
|
if (typeof ResizeObserver === 'undefined') return
|
|
218
280
|
|
|
281
|
+
// Apply measurements and anchor corrections while the previous layout is
|
|
282
|
+
// still current, but commit the resulting render after ResizeObserver.
|
|
283
|
+
let frame: number | null = null
|
|
219
284
|
const observer = new ResizeObserver((records) => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
285
|
+
resizeDeliveryRef.current = true
|
|
286
|
+
try {
|
|
287
|
+
for (const record of records) {
|
|
288
|
+
const meta = metaByNodeRef.current.get(record.target)
|
|
289
|
+
if (meta) handlersRef.current(meta, record)
|
|
290
|
+
}
|
|
291
|
+
} finally {
|
|
292
|
+
resizeDeliveryRef.current = false
|
|
223
293
|
}
|
|
294
|
+
|
|
295
|
+
if (!refreshAfterResizeRef.current || frame !== null) return
|
|
296
|
+
refreshAfterResizeRef.current = false
|
|
297
|
+
frame = requestAnimationFrame(() => {
|
|
298
|
+
frame = null
|
|
299
|
+
refreshWindow()
|
|
300
|
+
})
|
|
224
301
|
})
|
|
302
|
+
|
|
225
303
|
observerRef.current = observer
|
|
226
304
|
for (const node of nodesBySlotRef.current.values()) observer.observe(node)
|
|
227
305
|
|
|
228
306
|
return () => {
|
|
229
307
|
observer.disconnect()
|
|
308
|
+
if (frame !== null) cancelAnimationFrame(frame)
|
|
309
|
+
refreshAfterResizeRef.current = false
|
|
230
310
|
observerRef.current = null
|
|
231
311
|
}
|
|
232
312
|
}, [])
|
|
@@ -240,7 +320,33 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
240
320
|
const range = currentRange()
|
|
241
321
|
rangeRef.current = range
|
|
242
322
|
void windowVersion
|
|
243
|
-
const
|
|
323
|
+
const cells = cellsRef.current.update(entries.slice(range.start, range.end)).map((cell) => ({
|
|
324
|
+
...cell, top: cell.entry ? sizeIndexRef.current.prefix(cell.entry.index) : 0,
|
|
325
|
+
}))
|
|
326
|
+
|
|
327
|
+
const contentHeight = sizeIndexRef.current.total
|
|
328
|
+
for (const cell of cells) {
|
|
329
|
+
const node = nodesBySlotRef.current.get('cell:' + cell.id)
|
|
330
|
+
if (node) metaByNodeRef.current.set(node, {
|
|
331
|
+
kind: 'row', rowKey: cell.entry?.rowKey ?? '', cellId: cell.id, generation: cell.generation,
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
for (const cell of cells) {
|
|
337
|
+
if (!cell.entry || measuredAssignmentsRef.current.get(cell.id) === cell.generation) continue
|
|
338
|
+
const node = nodesBySlotRef.current.get('cell:' + cell.id)
|
|
339
|
+
if (!node || !cellsRef.current.isCurrent(cell.id, cell.entry.rowKey, cell.generation)) continue
|
|
340
|
+
const bounds = node.getBoundingClientRect()
|
|
341
|
+
measuredAssignmentsRef.current.set(cell.id, cell.generation)
|
|
342
|
+
recordRowSize(cell.entry.rowKey, bounds.width, bounds.height)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
for (const id of measuredAssignmentsRef.current.keys()) {
|
|
346
|
+
if (!nodesBySlotRef.current.has('cell:' + id)) measuredAssignmentsRef.current.delete(id)
|
|
347
|
+
}
|
|
348
|
+
}, [cells])
|
|
349
|
+
|
|
244
350
|
const style = {
|
|
245
351
|
...props.style,
|
|
246
352
|
minHeight: props.style?.minHeight ?? 0,
|
|
@@ -280,26 +386,26 @@ export function VirtualList<T = any>(props: VirtualListProps<T>) @{
|
|
|
280
386
|
{entries.length === 0 ? (
|
|
281
387
|
props.renderEmpty ? <div className="vx-virtual-list-empty">{props.renderEmpty()}</div> : null
|
|
282
388
|
) : (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
@for (const entry of visibleEntries; key entry.rowKey) {
|
|
389
|
+
<div className="vx-virtual-list-body" style={{ position: 'relative', height: contentHeight }}>
|
|
390
|
+
@for (const cell of cells; key cell.id) {
|
|
286
391
|
<div
|
|
287
|
-
className=
|
|
288
|
-
data-vx-row=
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
{ kind: 'row', rowKey: entry.rowKey },
|
|
293
|
-
)}
|
|
392
|
+
className={cell.entry ? 'vx-virtual-list-cell vx-virtual-list-row' : 'vx-virtual-list-cell'}
|
|
393
|
+
data-vx-row={cell.entry ? 'true' : undefined}
|
|
394
|
+
hidden={!cell.entry}
|
|
395
|
+
style={{ position: 'absolute', top: cell.top, left: 0, width: '100%' }}
|
|
396
|
+
ref={cellCallback(cell.id)}
|
|
294
397
|
>
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
398
|
+
@for (const entry of cell.rows; key entry.rowKey) {
|
|
399
|
+
<>
|
|
400
|
+
<div className="vx-virtual-list-item">{props.renderItem(entry.item, entry.index)}</div>
|
|
401
|
+
{hasSeparator && entry.index < entries.length - 1 ? (
|
|
402
|
+
<div className="vx-virtual-list-separator">{props.renderSeparator?.()}</div>
|
|
403
|
+
) : null}
|
|
404
|
+
</>
|
|
405
|
+
}
|
|
299
406
|
</div>
|
|
300
407
|
}
|
|
301
|
-
|
|
302
|
-
</>
|
|
408
|
+
</div>
|
|
303
409
|
)}
|
|
304
410
|
{props.renderFooter ? (
|
|
305
411
|
<div
|
package/src/WebView.tsrx
CHANGED
|
@@ -16,6 +16,7 @@ function reportContentLayout(view: any, props: WebViewProps) {
|
|
|
16
16
|
} else if (isAndroid) {
|
|
17
17
|
height = Number(view?.android?.getContentHeight?.() ?? 0) * Number(view?.android?.getScale?.() ?? 1)
|
|
18
18
|
}
|
|
19
|
+
|
|
19
20
|
if (!Number.isFinite(width) || !Number.isFinite(height) || height <= 0) return
|
|
20
21
|
props.onLayoutContent?.({ width, height })
|
|
21
22
|
if (props.matchContents) {
|
|
@@ -24,6 +25,22 @@ function reportContentLayout(view: any, props: WebViewProps) {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
function applyNativeScrollEnabled(view: any, enabled: boolean | undefined) {
|
|
29
|
+
if (isIOS) {
|
|
30
|
+
const scrollView = view?.ios?.scrollView
|
|
31
|
+
if (scrollView) scrollView.scrollEnabled = enabled !== false
|
|
32
|
+
} else if (isAndroid) {
|
|
33
|
+
const nativeView = view?.android
|
|
34
|
+
if (!nativeView?.setOnTouchListener) return
|
|
35
|
+
nativeView.setOnTouchListener(enabled === false
|
|
36
|
+
? new android.view.View.OnTouchListener({
|
|
37
|
+
onTouch: (_view: any, event: any) =>
|
|
38
|
+
event.getActionMasked() === android.view.MotionEvent.ACTION_MOVE,
|
|
39
|
+
})
|
|
40
|
+
: null)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
27
44
|
/** Embedded web document — NS <webview> (WKWebView / android.webkit.WebView).
|
|
28
45
|
* `html` flows through the `src` property: values that aren't http/file
|
|
29
46
|
* URLs take its `_loadData` path. NS reports failures as `loadFinished`
|
|
@@ -39,16 +56,7 @@ export function WebView(props: WebViewProps) @{
|
|
|
39
56
|
useEffect(() => {
|
|
40
57
|
const v = el.current;
|
|
41
58
|
if (!v) return;
|
|
42
|
-
|
|
43
|
-
v.ios.scrollView.scrollEnabled = props.scrollEnabled !== false;
|
|
44
|
-
} else if (isAndroid) {
|
|
45
|
-
v.android.setOnTouchListener(props.scrollEnabled === false
|
|
46
|
-
? new android.view.View.OnTouchListener({
|
|
47
|
-
onTouch: (_view: any, event: any) =>
|
|
48
|
-
event.getActionMasked() === android.view.MotionEvent.ACTION_MOVE,
|
|
49
|
-
})
|
|
50
|
-
: null);
|
|
51
|
-
}
|
|
59
|
+
applyNativeScrollEnabled(v, props.scrollEnabled)
|
|
52
60
|
if (props.matchContents && loaded.current) reportContentLayout(v, props);
|
|
53
61
|
}, [props.scrollEnabled, props.matchContents]);
|
|
54
62
|
|
|
@@ -70,7 +78,9 @@ export function WebView(props: WebViewProps) @{
|
|
|
70
78
|
className={cx('vx-webview', props.className)}
|
|
71
79
|
style={props.style}
|
|
72
80
|
src={props.html !== undefined ? props.html : props.src}
|
|
81
|
+
onLoaded={() => applyNativeScrollEnabled(el.current, props.scrollEnabled)}
|
|
73
82
|
onLoadFinished={(e: any) => {
|
|
83
|
+
applyNativeScrollEnabled(e.object ?? el.current, props.scrollEnabled)
|
|
74
84
|
if (e.error) props.onError?.({ url: e.url, error: e.error });
|
|
75
85
|
else {
|
|
76
86
|
loaded.current = true;
|
package/src/WebView.web.tsrx
CHANGED
|
@@ -33,6 +33,7 @@ export function WebView(props: WebViewProps) @{
|
|
|
33
33
|
},
|
|
34
34
|
native: el,
|
|
35
35
|
});
|
|
36
|
+
|
|
36
37
|
useEffect(() => {
|
|
37
38
|
const el = frame.current;
|
|
38
39
|
if (!el) return;
|
|
@@ -48,6 +49,7 @@ export function WebView(props: WebViewProps) @{
|
|
|
48
49
|
width: Math.max(body.scrollWidth, root.scrollWidth),
|
|
49
50
|
height: Math.max(body.scrollHeight, root.scrollHeight),
|
|
50
51
|
};
|
|
52
|
+
|
|
51
53
|
if (size.width <= 0 || size.height <= 0) return;
|
|
52
54
|
if (props.matchContents) el.style.height = `${size.height}px`;
|
|
53
55
|
props.onLayoutContent?.(size);
|
|
@@ -62,6 +64,7 @@ export function WebView(props: WebViewProps) @{
|
|
|
62
64
|
// Cross-origin documents cannot be inspected by the embedding page.
|
|
63
65
|
}
|
|
64
66
|
};
|
|
67
|
+
|
|
65
68
|
el.addEventListener('load', measure);
|
|
66
69
|
measure();
|
|
67
70
|
return () => {
|
|
@@ -31,6 +31,7 @@ export function MaterialDialog(props: ModalProps) @{
|
|
|
31
31
|
console.warn('[Modal] open dropped — no live presenter (currentPage mid-navigation or no active frame).');
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
|
+
|
|
34
35
|
const host = new GridLayout();
|
|
35
36
|
const unbindTheme = applyThemeClasses(host, 'vx-modal-host');
|
|
36
37
|
const root = createNativeScriptRoot(host);
|
package/src/anim.macos.tsrx
CHANGED
|
@@ -2,7 +2,10 @@ import type { NativeModifier } from './props'
|
|
|
2
2
|
|
|
3
3
|
/** Apply serializable modifiers to a NativeScript host view in array order. */
|
|
4
4
|
export function applyNativeModifiers(view: any, modifiers?: readonly NativeModifier[]): void {
|
|
5
|
-
if (!view || !modifiers?.length)
|
|
5
|
+
if (!view || !modifiers?.length) {
|
|
6
|
+
return
|
|
7
|
+
}
|
|
8
|
+
|
|
6
9
|
for (const entry of modifiers) {
|
|
7
10
|
if (entry.type === 'style') {
|
|
8
11
|
Object.assign(view.style, entry.values)
|
package/src/child-array.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/** Normalize a JSX children value to a flat array — children may be a
|
|
2
2
|
* single node, an array, or nested arrays from `.map` calls. */
|
|
3
3
|
export function toChildArray(children: any): any[] {
|
|
4
|
-
const out: any[] = []
|
|
4
|
+
const out: any[] = []
|
|
5
5
|
const walk = (node: any) => {
|
|
6
|
-
if (node == null || node === false) {
|
|
6
|
+
if (node == null || node === false) {
|
|
7
|
+
return
|
|
8
|
+
}
|
|
9
|
+
|
|
7
10
|
if (Array.isArray(node)) {
|
|
8
|
-
for (const child of node) {
|
|
11
|
+
for (const child of node) {
|
|
12
|
+
walk(child)
|
|
13
|
+
}
|
|
9
14
|
} else {
|
|
10
|
-
out.push(node)
|
|
15
|
+
out.push(node)
|
|
11
16
|
}
|
|
12
|
-
}
|
|
17
|
+
}
|
|
13
18
|
|
|
14
|
-
walk(children)
|
|
15
|
-
return out
|
|
19
|
+
walk(children)
|
|
20
|
+
return out
|
|
16
21
|
}
|
package/src/colorScheme.macos.ts
CHANGED
package/src/controls.macos.tsrx
CHANGED
|
@@ -24,6 +24,7 @@ export function Button(props: ButtonProps) @{
|
|
|
24
24
|
export function Checkbox(props: CheckboxProps) @{
|
|
25
25
|
const checked = props.checked ?? false
|
|
26
26
|
const colors = controlColors(useThemeScheme() === 'dark')
|
|
27
|
+
|
|
27
28
|
<flexboxlayout
|
|
28
29
|
id={props.id}
|
|
29
30
|
flexDirection="row"
|
|
@@ -49,7 +50,7 @@ export function Checkbox(props: CheckboxProps) @{
|
|
|
49
50
|
...(checked ? { backgroundColor: colors.primary } : {}),
|
|
50
51
|
}}
|
|
51
52
|
>
|
|
52
|
-
{checked ? <Text className="vx-checkbox-check" style={{ fontFamily: 'system-ui', fontSize: 14, lineHeight:
|
|
53
|
+
{checked ? <Text className="vx-checkbox-check" style={{ fontFamily: 'system-ui', fontSize: 14, lineHeight: '20px', color: colors.onPrimary }}>✓</Text> : null}
|
|
53
54
|
</flexboxlayout>
|
|
54
55
|
{props.label != null ? <Text className="vx-checkbox-label">{props.label}</Text> : null}
|
|
55
56
|
</flexboxlayout>
|
|
@@ -58,6 +59,7 @@ export function Checkbox(props: CheckboxProps) @{
|
|
|
58
59
|
export function Switch(props: SwitchProps) @{
|
|
59
60
|
const checked = props.checked ?? false
|
|
60
61
|
const colors = controlColors(useThemeScheme() === 'dark')
|
|
62
|
+
|
|
61
63
|
<flexboxlayout
|
|
62
64
|
id={props.id}
|
|
63
65
|
flexDirection="row"
|
|
@@ -108,6 +110,7 @@ export function Slider(props: SliderProps) @{
|
|
|
108
110
|
setDragValue(null)
|
|
109
111
|
}
|
|
110
112
|
})
|
|
113
|
+
|
|
111
114
|
const adjustValue = (direction: number) => {
|
|
112
115
|
if (props.disabled) {return}
|
|
113
116
|
const next = Math.min(max, Math.max(min, value + direction * ((max - min) / 100)))
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { expect, it, vi } from 'vitest'
|
|
2
|
+
import { createUniversalRoot, flushUniversalSync } from 'octane/universal/native'
|
|
3
|
+
import { Retained } from '../tests/data-lifecycle.fixture.mobile.tsrx'
|
|
4
|
+
|
|
5
|
+
// Only the NativeScript host objects are substituted. The installed driver,
|
|
6
|
+
// compiler output, scheduler, events, and visibility commands are real.
|
|
7
|
+
vi.mock('@nativescript/core', () => {
|
|
8
|
+
class View {
|
|
9
|
+
visibility = 'visible'
|
|
10
|
+
parent: View | null = null
|
|
11
|
+
text = ''
|
|
12
|
+
listeners = new Map<string, Set<(data: unknown) => void>>()
|
|
13
|
+
on(type: string, fn: (data: unknown) => void) {
|
|
14
|
+
if (!this.listeners.has(type)) {
|
|
15
|
+
this.listeners.set(type, new Set())
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
this.listeners.get(type)!.add(fn)
|
|
19
|
+
}
|
|
20
|
+
off(type: string, fn: (data: unknown) => void) {
|
|
21
|
+
this.listeners.get(type)?.delete(fn)
|
|
22
|
+
}
|
|
23
|
+
notify(data: { eventName: string }) {
|
|
24
|
+
for (const fn of this.listeners.get(data.eventName) ?? []) {
|
|
25
|
+
fn(data)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class LayoutBase extends View {
|
|
31
|
+
children: View[] = []
|
|
32
|
+
addChild(view: View) {
|
|
33
|
+
this.children.push(view)
|
|
34
|
+
view.parent = this
|
|
35
|
+
}
|
|
36
|
+
insertChild(view: View, index: number) {
|
|
37
|
+
this.children.splice(index, 0, view)
|
|
38
|
+
view.parent = this
|
|
39
|
+
}
|
|
40
|
+
removeChild(view: View) {
|
|
41
|
+
this.children.splice(this.children.indexOf(view), 1)
|
|
42
|
+
view.parent = null
|
|
43
|
+
}
|
|
44
|
+
getChildrenCount() {
|
|
45
|
+
return this.children.length
|
|
46
|
+
}
|
|
47
|
+
getChildIndex(view: View) {
|
|
48
|
+
return this.children.indexOf(view)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class ContentView extends View {
|
|
53
|
+
content: View | null = null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class TextBase extends View {}
|
|
57
|
+
const named = Object.fromEntries(
|
|
58
|
+
'AbsoluteLayout ActionBar ActionItem ActivityIndicator Button LiquidGlass DatePicker DockLayout FlexboxLayout FormattedString Frame GridLayout HtmlView Image Label ListPicker ListView NavigationButton Page Placeholder Progress ProxyViewContainer RootLayout ScrollView SearchBar SegmentedBar SegmentedBarItem Slider Span StackLayout Switch TabView TabViewItem TextField TextView TimePicker ViewBase WebView WrapLayout'
|
|
59
|
+
.split(' ')
|
|
60
|
+
.map((name) => [name, class extends View {}]),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
...named,
|
|
65
|
+
View,
|
|
66
|
+
LayoutBase,
|
|
67
|
+
ContentView,
|
|
68
|
+
TextBase,
|
|
69
|
+
Button: TextBase,
|
|
70
|
+
Label: TextBase,
|
|
71
|
+
unsetValue: Symbol('unset'),
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('NativeScript driver permits retained suspense and delivers native taps and uncaught errors', async () => {
|
|
76
|
+
// A direct driver import bypasses the object-driver alias for the renderer
|
|
77
|
+
// entry. This is the installed package that the mobile app actually uses.
|
|
78
|
+
const { nativeScriptDriver, createNativeScriptContainer, releaseNativeScriptContainer } =
|
|
79
|
+
await import('../node_modules/@nativescript-community/octane/dist/driver.js')
|
|
80
|
+
|
|
81
|
+
const { LayoutBase } = await import('@nativescript/core')
|
|
82
|
+
expect(nativeScriptDriver.capabilities.visibility).toBe(true)
|
|
83
|
+
const host = new LayoutBase()
|
|
84
|
+
const container = createNativeScriptContainer(host)
|
|
85
|
+
const uncaught = vi.fn()
|
|
86
|
+
const warning = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
87
|
+
const root = createUniversalRoot(container, nativeScriptDriver, { onUncaughtError: uncaught })
|
|
88
|
+
container.root = root
|
|
89
|
+
let resolve!: (value: string) => void
|
|
90
|
+
const pending = new Promise<string>((done) => {
|
|
91
|
+
resolve = done
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const error = new Error('native handler failure')
|
|
95
|
+
const fail = vi.fn(() => {
|
|
96
|
+
throw error
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
root.render(Retained, { pending, fail })
|
|
101
|
+
const body = (host as any).children[0]
|
|
102
|
+
body.notify({ eventName: 'tap' })
|
|
103
|
+
flushUniversalSync(() => {})
|
|
104
|
+
expect((host as any).children[0]).toBe(body)
|
|
105
|
+
expect(body.visibility).toBe('collapse')
|
|
106
|
+
expect((host as any).children[1].text).toBe('pending')
|
|
107
|
+
resolve('resolved')
|
|
108
|
+
await vi.waitFor(() => expect(body.text).toBe('resolved'))
|
|
109
|
+
expect(body.visibility).toBe('visible')
|
|
110
|
+
|
|
111
|
+
;(host as any).children[1].notify({ eventName: 'tap' })
|
|
112
|
+
expect(fail).toHaveBeenCalledOnce()
|
|
113
|
+
expect(uncaught).toHaveBeenCalledWith(error)
|
|
114
|
+
expect(warning).not.toHaveBeenCalled()
|
|
115
|
+
} finally {
|
|
116
|
+
root.unmount()
|
|
117
|
+
releaseNativeScriptContainer(container)
|
|
118
|
+
warning.mockRestore()
|
|
119
|
+
}
|
|
120
|
+
})
|