@onekeyfe/react-native-native-list 3.0.174 → 3.0.176
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/docs/SPEC.md
CHANGED
|
@@ -86,6 +86,9 @@ and scrollbar estimates MUST NOT be mistaken for data-row reading anchors.
|
|
|
86
86
|
|
|
87
87
|
An accepted imperative command owns its request, execution, pending-layout and
|
|
88
88
|
posted-alignment work. Invalid commands MUST NOT cancel a valid request.
|
|
89
|
+
On iOS, known-configuration validation MUST precede changing accepted, pending
|
|
90
|
+
or animated-command state; invalid input MUST preserve the existing terminal
|
|
91
|
+
notification, while configuration-unavailable requests retain deferred validation.
|
|
89
92
|
Completion on Android requires the current request to have finished dispatch,
|
|
90
93
|
its own pending layout/alignment barriers to have completed, and native scrolling
|
|
91
94
|
to be idle without an active layout-manager animation. A true animated top
|
|
@@ -657,28 +657,35 @@ final class NativeListView: UIView {
|
|
|
657
657
|
requestScroll(.end(animated))
|
|
658
658
|
}
|
|
659
659
|
|
|
660
|
-
private func
|
|
661
|
-
guard
|
|
660
|
+
private func explicitTopForScrollCommand(_ request: ScrollRequest) -> Bool? {
|
|
661
|
+
guard let config else { return nil }
|
|
662
662
|
let explicitTop: Bool
|
|
663
663
|
switch request {
|
|
664
664
|
case let .key(key, _, alignment, viewPosition, viewOffset):
|
|
665
|
-
guard config.items.contains(where: { $0.key == key }), viewPosition.isFinite, viewOffset.isFinite else { return }
|
|
665
|
+
guard config.items.contains(where: { $0.key == key }), viewPosition.isFinite, viewOffset.isFinite else { return nil }
|
|
666
666
|
explicitTop = config.items.first?.key == key && alignment == "start" && viewPosition == 0 && viewOffset == 0
|
|
667
667
|
case let .index(index, _, alignment, viewPosition, viewOffset):
|
|
668
|
-
guard index >= 0, index < config.items.count, viewPosition.isFinite, viewOffset.isFinite else { return }
|
|
668
|
+
guard index >= 0, index < config.items.count, viewPosition.isFinite, viewOffset.isFinite else { return nil }
|
|
669
669
|
explicitTop = index == 0 && alignment == "start" && viewPosition == 0 && viewOffset == 0
|
|
670
670
|
case let .offset(offset, _):
|
|
671
|
-
guard offset.isFinite else { return }
|
|
671
|
+
guard offset.isFinite else { return nil }
|
|
672
672
|
explicitTop = offset <= 0
|
|
673
673
|
case .end:
|
|
674
674
|
explicitTop = false
|
|
675
675
|
}
|
|
676
|
+
return explicitTop
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
private func acceptScrollCommand(_ request: ScrollRequest) {
|
|
680
|
+
guard !commandAcceptedForPendingScroll,
|
|
681
|
+
let explicitTop = explicitTopForScrollCommand(request) else { return }
|
|
676
682
|
commandAcceptedForPendingScroll = true
|
|
677
683
|
NotificationCenter.default.post(name: Notification.Name("OneKeyNativeListScrollCommandAccepted"),
|
|
678
684
|
object: collectionView, userInfo: ["explicitTop": explicitTop, "phase": 0])
|
|
679
685
|
}
|
|
680
686
|
|
|
681
687
|
private func requestScroll(_ request: ScrollRequest) {
|
|
688
|
+
if config != nil && explicitTopForScrollCommand(request) == nil { return }
|
|
682
689
|
animatedExplicitScrollCommand = false
|
|
683
690
|
commandAcceptedForPendingScroll = false
|
|
684
691
|
acceptScrollCommand(request)
|
|
@@ -4,6 +4,63 @@ import XCTest
|
|
|
4
4
|
|
|
5
5
|
@MainActor
|
|
6
6
|
final class NativeListLifecycleTests: XCTestCase {
|
|
7
|
+
func testInvalidCommandsPreserveAcceptedAnimationCompletion() throws {
|
|
8
|
+
for invalid in 0..<3 {
|
|
9
|
+
let view = try makeScrollingList()
|
|
10
|
+
let window = UIWindow(frame: view.frame)
|
|
11
|
+
let controller = UIViewController()
|
|
12
|
+
window.rootViewController = controller
|
|
13
|
+
controller.view.addSubview(view)
|
|
14
|
+
window.isHidden = false
|
|
15
|
+
defer { window.isHidden = true }
|
|
16
|
+
window.layoutIfNeeded()
|
|
17
|
+
view.layoutIfNeeded()
|
|
18
|
+
drainMainQueue()
|
|
19
|
+
let list = try XCTUnwrap(view.subviews.compactMap { $0 as? UICollectionView }.first)
|
|
20
|
+
XCTAssertNotNil(view.window)
|
|
21
|
+
XCTAssertGreaterThan(list.contentSize.height - list.bounds.height +
|
|
22
|
+
list.adjustedContentInset.top + list.adjustedContentInset.bottom, 800)
|
|
23
|
+
let phases = NSMutableArray()
|
|
24
|
+
let observer = NotificationCenter.default.addObserver(
|
|
25
|
+
forName: Notification.Name("OneKeyNativeListScrollCommandAccepted"),
|
|
26
|
+
object: list, queue: nil
|
|
27
|
+
) { notification in
|
|
28
|
+
if let phase = notification.userInfo?["phase"] as? Int { phases.add(phase) }
|
|
29
|
+
}
|
|
30
|
+
defer { NotificationCenter.default.removeObserver(observer) }
|
|
31
|
+
view.scrollToOffset(800, animated: true)
|
|
32
|
+
XCTAssertEqual(phases as? [Int], [0, 1])
|
|
33
|
+
switch invalid {
|
|
34
|
+
case 0: view.scrollToKey("missing-fixture-key", animated: true, alignment: "start")
|
|
35
|
+
case 1: view.scrollToIndex(-1, animated: true, alignment: "start")
|
|
36
|
+
default: view.scrollToOffset(.nan, animated: true)
|
|
37
|
+
}
|
|
38
|
+
XCTAssertEqual(phases as? [Int], [0, 1], "Invalid input must not accept or end another command")
|
|
39
|
+
// Exercise the actual delegate terminal callback, not a copied state helper.
|
|
40
|
+
view.scrollViewDidEndScrollingAnimation(list)
|
|
41
|
+
XCTAssertEqual(phases as? [Int], [0, 1, 2])
|
|
42
|
+
view.scrollViewDidEndScrollingAnimation(list)
|
|
43
|
+
XCTAssertEqual(phases as? [Int], [0, 1, 2], "Completion is emitted once")
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func testInvalidKeyPreservesValidPendingScrollUntilGeometryArrives() throws {
|
|
48
|
+
let view = try makeScrollingList()
|
|
49
|
+
let list = try XCTUnwrap(view.subviews.compactMap { $0 as? UICollectionView }.first)
|
|
50
|
+
view.frame.size.height = 0
|
|
51
|
+
view.setNeedsLayout()
|
|
52
|
+
view.layoutIfNeeded()
|
|
53
|
+
XCTAssertEqual(list.bounds.height, 0)
|
|
54
|
+
view.scrollToOffset(500, animated: false)
|
|
55
|
+
view.scrollToKey("missing-fixture-key", animated: true, alignment: "start")
|
|
56
|
+
view.frame.size.height = 708
|
|
57
|
+
view.setNeedsLayout()
|
|
58
|
+
view.layoutIfNeeded()
|
|
59
|
+
list.layoutIfNeeded()
|
|
60
|
+
drainMainQueue()
|
|
61
|
+
XCTAssertEqual(list.contentOffset.y, 500 - list.adjustedContentInset.top, accuracy: 0.5)
|
|
62
|
+
}
|
|
63
|
+
|
|
7
64
|
func testSectionLoadingStopsOnRebindReuseAndDetach() throws {
|
|
8
65
|
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
|
|
9
66
|
let controller = UIViewController()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-native-list",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.176",
|
|
4
4
|
"description": "Template-driven native RecyclerView and UICollectionView for React Native",
|
|
5
5
|
"source": "./src/index.ts",
|
|
6
6
|
"main": "./lib/module/index.js",
|
|
@@ -87,8 +87,8 @@
|
|
|
87
87
|
"typescript": "^5.9.2"
|
|
88
88
|
},
|
|
89
89
|
"peerDependencies": {
|
|
90
|
-
"@onekeyfe/react-native-image": "3.0.
|
|
91
|
-
"@onekeyfe/react-native-native-logger": "3.0.
|
|
90
|
+
"@onekeyfe/react-native-image": "3.0.176",
|
|
91
|
+
"@onekeyfe/react-native-native-logger": "3.0.176",
|
|
92
92
|
"react": "*",
|
|
93
93
|
"react-native": "*",
|
|
94
94
|
"react-native-nitro-modules": "0.37.0"
|