@onekeyfe/react-native-native-list 3.0.174 → 3.0.175

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 acceptScrollCommand(_ request: ScrollRequest) {
661
- guard !commandAcceptedForPendingScroll, let config else { return }
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.174",
3
+ "version": "3.0.175",
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.174",
91
- "@onekeyfe/react-native-native-logger": "3.0.174",
90
+ "@onekeyfe/react-native-image": "3.0.175",
91
+ "@onekeyfe/react-native-native-logger": "3.0.175",
92
92
  "react": "*",
93
93
  "react-native": "*",
94
94
  "react-native-nitro-modules": "0.37.0"