@onekeyfe/react-native-native-list 6.7.0-alpha.398 → 6.7.0-alpha.400
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/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +33 -2
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +3 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +31 -8
- package/docs/ROW_TEMPLATES.md +4 -0
- package/docs/SPEC.md +161 -35
- package/ios/NativeListIdentityRenderer.swift +50 -3
- package/ios/RNCNativeListView.swift +6 -1
- package/ios/tests/NativeListLifecycleTests.swift +73 -4
- package/lib/module/NativeList.web.js +9 -12
- package/lib/module/models.js +2 -0
- package/lib/module/validation.js +627 -114
- package/lib/module/web/NativeListWebEngine.js +3 -9
- package/lib/module/web/templates/IdentityRowRenderer.js +58 -4
- package/lib/typescript/src/models.d.ts +25 -14
- package/lib/typescript/src/validation.d.ts +7 -2
- package/package.json +3 -3
- package/src/NativeList.web.tsx +10 -14
- package/src/models.ts +144 -153
- package/src/validation.ts +1617 -167
- package/src/web/NativeListWebEngine.ts +6 -10
- package/src/web/templates/IdentityRowRenderer.ts +70 -4
|
@@ -4,6 +4,25 @@ import XCTest
|
|
|
4
4
|
|
|
5
5
|
@MainActor
|
|
6
6
|
final class NativeListLifecycleTests: XCTestCase {
|
|
7
|
+
func testCanonicalNullPatchRemovesStoredOptionalFields() throws {
|
|
8
|
+
let view = NativeListView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
|
|
9
|
+
let row: [String: Any] = ["key": "token", "type": "identity", "title": "Token",
|
|
10
|
+
"leading": ["kind": "token"], "subtitle": "Old",
|
|
11
|
+
"titleImage": ["image": ["uri": "data:image/png;base64,AA==", "width": 14, "height": 14]]]
|
|
12
|
+
let snapshot: [String: Any] = ["schemaVersion": 1, "generation": 1,
|
|
13
|
+
"layout": ["kind": "linear"], "rows": [row]]
|
|
14
|
+
view.applySnapshotJson(String(decoding: try JSONSerialization.data(withJSONObject: snapshot), as: UTF8.self))
|
|
15
|
+
let patches: [[String: Any]] = [["key": "token", "type": "identity",
|
|
16
|
+
"changes": ["title": "New", "subtitle": NSNull(), "titleImage": NSNull()]]]
|
|
17
|
+
view.applyPatchesJson(String(decoding: try JSONSerialization.data(withJSONObject: patches), as: UTF8.self))
|
|
18
|
+
let items = try XCTUnwrap(Mirror(reflecting: view).descendant("itemsByKey") as? [String: NativeListItem])
|
|
19
|
+
let accepted = try XCTUnwrap(items["token"])
|
|
20
|
+
XCTAssertEqual(accepted.data.string("title"), "New")
|
|
21
|
+
XCTAssertNil(accepted.data["subtitle"])
|
|
22
|
+
XCTAssertNil(accepted.data["titleImage"])
|
|
23
|
+
XCTAssertEqual(accepted.key, "token")
|
|
24
|
+
}
|
|
25
|
+
|
|
7
26
|
func testAcceptedRowIndexReusesStableShapeAndRejectsLateInvalidPatch() throws {
|
|
8
27
|
let view = NativeListView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
|
|
9
28
|
func snapshot(_ keys: [String], generation: Int, title: String = "Old") throws -> String {
|
|
@@ -229,6 +248,47 @@ final class NativeListLifecycleTests: XCTestCase {
|
|
|
229
248
|
XCTAssertNotEqual(cell.bindingEpoch, changedEpoch)
|
|
230
249
|
}
|
|
231
250
|
|
|
251
|
+
func testIdentityTitleImageRetainsRequestAndClearsOnProductionCellReuse() throws {
|
|
252
|
+
let cell = NativeListIdentityCell(frame: CGRect(x: 0, y: 0, width: 320, height: 60))
|
|
253
|
+
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
|
|
254
|
+
window.addSubview(cell)
|
|
255
|
+
func bind(_ descriptor: [String: Any]?, title: String = "STOCK") throws {
|
|
256
|
+
var data: [String: Any] = ["key": "stock", "type": "identity", "title": title,
|
|
257
|
+
"height": 60, "leading": ["kind": "token", "fallbackIcon": ["name": "CryptoCoinOutline"]]]
|
|
258
|
+
if let descriptor { data["titleImage"] = descriptor }
|
|
259
|
+
cell.bind(item: try NativeListItem(data: data), theme: nil, layout: "linear", selected: false,
|
|
260
|
+
checkboxState: { _, _, fallback in fallback }, fonts: NativeListFontFamilies())
|
|
261
|
+
cell.layoutIfNeeded()
|
|
262
|
+
}
|
|
263
|
+
let descriptor: [String: Any] = ["image": ["uri": "data:image/png;base64,AA==", "width": 14, "height": 14],
|
|
264
|
+
"accessibilityLabel": "Issuer", "hoverActionKey": "issuer-info"]
|
|
265
|
+
try bind(descriptor)
|
|
266
|
+
let slot = try XCTUnwrap(Mirror(reflecting: cell).children.first {
|
|
267
|
+
$0.label == "titleImageSlot"
|
|
268
|
+
}?.value as? NativeListImageSlot)
|
|
269
|
+
XCTAssertFalse(slot.view.isHidden)
|
|
270
|
+
XCTAssertEqual(slot.view.bounds.width, 14, accuracy: 0.5)
|
|
271
|
+
XCTAssertEqual(slot.view.bounds.height, 14, accuracy: 0.5)
|
|
272
|
+
XCTAssertEqual(slot.view.layer.cornerRadius, 7, accuracy: 0.1)
|
|
273
|
+
XCTAssertEqual(slot.view.accessibilityLabel, "Issuer")
|
|
274
|
+
let identity = Mirror(reflecting: slot).children.first { $0.label == "identity" }?.value as? String
|
|
275
|
+
XCTAssertNotNil(identity)
|
|
276
|
+
try bind(descriptor, title: "Changed title")
|
|
277
|
+
XCTAssertEqual(Mirror(reflecting: slot).children.first { $0.label == "identity" }?.value as? String, identity)
|
|
278
|
+
try bind(["image": ["uri": "data:image/png;base64,AQ==", "width": 20, "height": 20]])
|
|
279
|
+
XCTAssertEqual(slot.view.bounds.width, 20, accuracy: 0.5)
|
|
280
|
+
XCTAssertEqual(slot.view.layer.cornerRadius, 10, accuracy: 0.1)
|
|
281
|
+
XCTAssertNil(slot.view.accessibilityLabel)
|
|
282
|
+
XCTAssertNotEqual(Mirror(reflecting: slot).children.first { $0.label == "identity" }?.value as? String, identity)
|
|
283
|
+
try bind(nil)
|
|
284
|
+
XCTAssertTrue(slot.view.isHidden)
|
|
285
|
+
XCTAssertNil(Mirror(reflecting: slot).children.first { $0.label == "identity" }?.value as? String)
|
|
286
|
+
try bind(descriptor)
|
|
287
|
+
cell.prepareForReuse()
|
|
288
|
+
XCTAssertTrue(slot.view.isHidden)
|
|
289
|
+
XCTAssertNil(slot.view.accessibilityLabel)
|
|
290
|
+
}
|
|
291
|
+
|
|
232
292
|
func testRiskIdentityTitleIconAndButtonResetOnProductionCellBind() throws {
|
|
233
293
|
let cell = NativeListIdentityCell(frame: CGRect(x: 0, y: 0, width: 320, height: 56))
|
|
234
294
|
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
|
|
@@ -1559,12 +1619,15 @@ final class NativeListLifecycleTests: XCTestCase {
|
|
|
1559
1619
|
let thousandCells = try sampleCells(count: 1000)
|
|
1560
1620
|
print("NativeList 100 visible/attached first/mid/tail: \(hundredCells)")
|
|
1561
1621
|
print("NativeList 1000 visible/attached first/mid/tail: \(thousandCells)")
|
|
1622
|
+
let visibleCellLimit = Int(ceil(list.bounds.height / 60)) + 2
|
|
1562
1623
|
XCTAssertTrue(hundredCells.allSatisfy {
|
|
1563
|
-
$0.visible > 0 && $0.visible <=
|
|
1624
|
+
$0.visible > 0 && $0.visible <= visibleCellLimit && $0.attached >= $0.visible
|
|
1564
1625
|
})
|
|
1565
1626
|
XCTAssertTrue(thousandCells.allSatisfy {
|
|
1566
|
-
$0.visible > 0 && $0.visible <=
|
|
1627
|
+
$0.visible > 0 && $0.visible <= visibleCellLimit && $0.attached >= $0.visible
|
|
1567
1628
|
})
|
|
1629
|
+
// UIKit also retains offscreen cells in subviews. Their count may exceed
|
|
1630
|
+
// the visible budget, but must remain bounded as the data grows tenfold.
|
|
1568
1631
|
XCTAssertLessThanOrEqual(thousandCells.map(\.attached).max() ?? 0,
|
|
1569
1632
|
(hundredCells.map(\.attached).max() ?? 0) + 4)
|
|
1570
1633
|
}
|
|
@@ -1881,6 +1944,7 @@ final class NativeListLifecycleTests: XCTestCase {
|
|
|
1881
1944
|
for (index, slot) in slots.enumerated() { view.mountContainerSlot(slot, atIndex: index) }
|
|
1882
1945
|
view.setContainerSlotHeightsJson("[0,0,140,0,100,40]")
|
|
1883
1946
|
var settlements: [(String, String, Bool)] = []
|
|
1947
|
+
let latestSettled = expectation(description: "Latest disclosure transition settled")
|
|
1884
1948
|
view.onStructuralTransitionSettled = { payload in
|
|
1885
1949
|
guard let data = payload.data(using: .utf8),
|
|
1886
1950
|
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
@@ -1888,6 +1952,7 @@ final class NativeListLifecycleTests: XCTestCase {
|
|
|
1888
1952
|
let status = object["status"] as? String,
|
|
1889
1953
|
let expanded = object["settledExpanded"] as? Bool else { return }
|
|
1890
1954
|
settlements.append((id, status, expanded))
|
|
1955
|
+
if id == "collapse-latest" { latestSettled.fulfill() }
|
|
1891
1956
|
}
|
|
1892
1957
|
view.applySnapshotJson(try structuralRowsSnapshot(count: 6, generation: 1,
|
|
1893
1958
|
disclosureExpanded: false))
|
|
@@ -1908,11 +1973,15 @@ final class NativeListLifecycleTests: XCTestCase {
|
|
|
1908
1973
|
view.applySnapshotJson(try structuralRowsSnapshot(count: 50, generation: 4,
|
|
1909
1974
|
transitionID: "expand-superseded", durationMs: 400,
|
|
1910
1975
|
disclosureExpanded: true))
|
|
1911
|
-
|
|
1976
|
+
// Supersede on the same main-thread turn: a timed wait can outlast the
|
|
1977
|
+
// expansion on a busy simulator and legitimately deliver its settlement.
|
|
1978
|
+
XCTAssertNotNil(Mirror(reflecting: view).descendant("structuralDisplayLink")
|
|
1979
|
+
as? CADisplayLink)
|
|
1980
|
+
XCTAssertEqual(settlements.map(\.0), ["same-rows"])
|
|
1912
1981
|
view.applySnapshotJson(try structuralRowsSnapshot(count: 6, generation: 5,
|
|
1913
1982
|
transitionID: "collapse-latest", durationMs: 400,
|
|
1914
1983
|
disclosureExpanded: false))
|
|
1915
|
-
|
|
1984
|
+
wait(for: [latestSettled], timeout: 2)
|
|
1916
1985
|
XCTAssertEqual(settlements.map(\.0), ["same-rows", "collapse-latest"])
|
|
1917
1986
|
XCTAssertEqual(settlements.last?.1, "completed")
|
|
1918
1987
|
XCTAssertEqual(settlements.last?.2, false)
|
|
@@ -4,7 +4,7 @@ import { useNativeListFontFamiliesJson } from "./fonts.js";
|
|
|
4
4
|
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import { View } from 'react-native';
|
|
6
6
|
import { normalizeIndexScroll, normalizeKeyScroll, normalizePositionScroll, resolveLocationIndex, scrollFailure, validateOffset } from "./scrolling.js";
|
|
7
|
-
import { applyRowPatches, containNativeListInput, emptyNativeListSnapshot,
|
|
7
|
+
import { applyRowPatches, containNativeListInput, emptyNativeListSnapshot, validateSnapshot } from "./validation.js";
|
|
8
8
|
import { NativeListWebEngine } from "./web/NativeListWebEngine.js";
|
|
9
9
|
import { acquireNativeListAvatar, canonicalNativeListAvatarUri } from "./web/NativeListWebAvatarCache.js";
|
|
10
10
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -255,18 +255,15 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
|
|
|
255
255
|
synchronizeSlots(next);
|
|
256
256
|
},
|
|
257
257
|
applyPatches(patches) {
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const next = containNativeListInput(() => applyRowPatches(snapshotRef.current, accepted));
|
|
266
|
-
if (next) snapshotRef.current = next;
|
|
267
|
-
}
|
|
268
|
-
acceptedSnapshotRef.current = snapshotRef.current;
|
|
258
|
+
const engine = engineRef.current;
|
|
259
|
+
if (engine) {
|
|
260
|
+
engine.applyPatches(patches);
|
|
261
|
+
snapshotRef.current = engine.currentSnapshot();
|
|
262
|
+
} else {
|
|
263
|
+
const next = containNativeListInput(() => applyRowPatches(snapshotRef.current, patches));
|
|
264
|
+
if (next) snapshotRef.current = next;
|
|
269
265
|
}
|
|
266
|
+
acceptedSnapshotRef.current = snapshotRef.current;
|
|
270
267
|
},
|
|
271
268
|
reconcileSelection(selectedKeys) {
|
|
272
269
|
engineRef.current?.reconcileSelection(selectedKeys);
|
package/lib/module/models.js
CHANGED
|
@@ -54,4 +54,6 @@ export const ROW_BOX_STYLE_KEYS_BY_TYPE = {
|
|
|
54
54
|
// type CommonPatchFields = 'revision' | 'disabled' | 'selected' | 'separator';
|
|
55
55
|
// OneKey patch: balance patches also refresh the existing row's spoken content.
|
|
56
56
|
// type CommonPatchFields = 'revision' | 'disabled' | 'selected' | 'separator' | 'height' | 'heightRounding' | 'opacity' | 'pressDisabled';
|
|
57
|
+
|
|
58
|
+
/** Null removes optional fields; undefined leaves the existing value unchanged. */
|
|
57
59
|
//# sourceMappingURL=models.js.map
|