@lodev09/react-native-true-sheet 4.0.0-beta.26 → 4.0.0-beta.27

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.
@@ -32,10 +32,24 @@ class TrueSheetFooterView(reactContext: ThemedReactContext) : ReactViewGroup(rea
32
32
 
33
33
  private var lastWidth = 0
34
34
  private var lastHeight = 0
35
+ private var stateTranslationY = 0f
35
36
 
36
37
  var bottomInset = 0
37
38
  private set
38
39
 
40
+ /**
41
+ * positionFooter slides the footer off its Yoga position (above the
42
+ * keyboard, or riding the sheet). Mirror where it rests into the shadow
43
+ * node so measure() — and Pressability's press region — match where it's
44
+ * drawn. Called once the footer settles: every push is a commit.
45
+ */
46
+ fun syncTranslationState() {
47
+ if (translationY == stateTranslationY) return
48
+ val sw = stateWrapper ?: return
49
+ stateTranslationY = translationY
50
+ TrueSheetStateUpdater.updateFooterTranslation(sw, translationY.pxToDp())
51
+ }
52
+
39
53
  /**
40
54
  * Height the footer occupies above the keyboard — its layout height minus
41
55
  * the safe-area inset, which rides below the keyboard's top edge while the
@@ -55,14 +69,18 @@ class TrueSheetFooterView(reactContext: ThemedReactContext) : ReactViewGroup(rea
55
69
 
56
70
  val sw = stateWrapper ?: return
57
71
  val insetDp = bottomInset.toFloat().pxToDp()
72
+ // Carried along — this update replaces a still-queued translation update
73
+ stateTranslationY = translationY
74
+ val translateYDp = translationY.pxToDp()
58
75
 
59
76
  // Synchronous update — the footer must be padded before detents are
60
77
  // configured, otherwise the auto detent is set up an inset short
61
- if (TrueSheetStateUpdater.updateFooterState(sw, insetDp)) return
78
+ if (TrueSheetStateUpdater.updateFooterState(sw, insetDp, translateYDp)) return
62
79
 
63
80
  // Fallback: async state update
64
81
  val newState = WritableNativeMap()
65
82
  newState.putDouble("bottomInset", insetDp.toDouble())
83
+ newState.putDouble("translateY", translateYDp.toDouble())
66
84
  sw.updateState(newState)
67
85
  }
68
86
 
@@ -41,8 +41,13 @@ object TrueSheetStateUpdater {
41
41
  }
42
42
 
43
43
  /** Returns false when unavailable — caller should fall back to async updateState. */
44
- fun updateFooterState(stateWrapper: StateWrapper, bottomInsetDp: Float): Boolean =
45
- isAvailable && nativeUpdateFooterState(stateWrapper, bottomInsetDp)
44
+ fun updateFooterState(stateWrapper: StateWrapper, bottomInsetDp: Float, translateYDp: Float): Boolean =
45
+ isAvailable && nativeUpdateFooterState(stateWrapper, bottomInsetDp, translateYDp)
46
+
47
+ /** Mirrors the footer's native translation so measure() matches where it's drawn. */
48
+ fun updateFooterTranslation(stateWrapper: StateWrapper, translateYDp: Float) {
49
+ if (isAvailable) nativeUpdateFooterTranslation(stateWrapper, translateYDp)
50
+ }
46
51
 
47
52
  /**
48
53
  * Publishes the precalculated bottom safe-area inset so a late-mounted
@@ -56,7 +61,10 @@ object TrueSheetStateUpdater {
56
61
  private external fun nativeUpdateState(stateWrapper: Any, width: Float, height: Float): Boolean
57
62
 
58
63
  @JvmStatic
59
- private external fun nativeUpdateFooterState(stateWrapper: Any, bottomInset: Float): Boolean
64
+ private external fun nativeUpdateFooterState(stateWrapper: Any, bottomInset: Float, translateY: Float): Boolean
65
+
66
+ @JvmStatic
67
+ private external fun nativeUpdateFooterTranslation(stateWrapper: Any, translateY: Float)
60
68
 
61
69
  @JvmStatic
62
70
  private external fun nativeSetBottomSafeArea(insetDp: Float)
@@ -374,6 +374,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
374
374
  private val isKeyboardTransitioning: Boolean
375
375
  get() = keyboardObserver?.isTransitioning ?: false
376
376
 
377
+ // The footer slides every frame until the sheet and keyboard come to rest
378
+ private val isSheetMoving: Boolean
379
+ get() = isPresentAnimating ||
380
+ isKeyboardTransitioning ||
381
+ behavior?.state == BottomSheetBehavior.STATE_DRAGGING ||
382
+ behavior?.state == BottomSheetBehavior.STATE_SETTLING
383
+
377
384
  // Keyboard is up or animating — detents are (or are about to be) keyboard-shifted
378
385
  val isKeyboardActive: Boolean
379
386
  get() = keyboardInset > 0 || isKeyboardTransitioning
@@ -724,6 +731,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
724
731
  }
725
732
 
726
733
  private fun handleStateSettled(sheetView: View, newState: Int) {
734
+ // The settle's last slide frame has positioned the footer
735
+ containerView?.footerView?.syncTranslationState()
736
+
727
737
  if (interactionState is InteractionState.Reconfiguring) return
728
738
 
729
739
  // A release already at a stop settles without a slide frame
@@ -1288,6 +1298,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1288
1298
  // absolute footer floats above it
1289
1299
  if (!absoluteFooter) {
1290
1300
  footerView.translationY = 0f
1301
+ footerView.syncTranslationState()
1291
1302
  return
1292
1303
  }
1293
1304
 
@@ -1313,6 +1324,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1313
1324
  // Clamp to prevent footer going above safe area
1314
1325
  val maxAllowedY = (sheetHeight - topInset - footerHeight).toFloat()
1315
1326
  footerView.y = minOf(footerY, maxAllowedY)
1327
+ // Mid-motion slides are synced once the sheet settles or the keyboard
1328
+ // lands (handleStateSettled, keyboardDidShow/keyboardDidHide)
1329
+ if (!isSheetMoving) footerView.syncTranslationState()
1316
1330
  if (!footerAvoidsKeyboard) {
1317
1331
  containerView?.contentView?.updateContentInset()
1318
1332
  }
@@ -1403,7 +1417,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
1403
1417
  )
1404
1418
  }
1405
1419
 
1420
+ override fun keyboardDidShow(height: Int) {
1421
+ // The keyboard's last frame has positioned the footer
1422
+ containerView?.footerView?.syncTranslationState()
1423
+ }
1424
+
1406
1425
  override fun keyboardDidHide() {
1426
+ containerView?.footerView?.syncTranslationState()
1407
1427
  if (!shouldHandleKeyboard(checkFocus = false)) return
1408
1428
  isKeyboardDismissProgrammatic = false
1409
1429
  // Keep detentIndexBeforeKeyboard for the settle's detent-change check
@@ -48,7 +48,14 @@ class TrueSheetOverlayRootView(private val reactContext: ThemedReactContext) :
48
48
  translationZ = OVERLAY_Z
49
49
  }
50
50
 
51
- // Children are positioned by Fabric — a FrameLayout pass would stretch them to match_parent
51
+ // Children are measured and positioned by Fabric — a FrameLayout pass would stretch them to match_parent
52
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
53
+ setMeasuredDimension(
54
+ MeasureSpec.getSize(widthMeasureSpec),
55
+ MeasureSpec.getSize(heightMeasureSpec)
56
+ )
57
+ }
58
+
52
59
  override fun onLayout(
53
60
  changed: Boolean,
54
61
  left: Int,
@@ -58,7 +58,8 @@ static jboolean updateStateImmediate(
58
58
  static jboolean updateFooterStateImmediate(
59
59
  jni::alias_ref<jclass> /*clazz*/,
60
60
  jni::alias_ref<jobject> stateWrapper,
61
- jfloat bottomInset) {
61
+ jfloat bottomInset,
62
+ jfloat translateY) {
62
63
  auto state = getStateFromWrapper(stateWrapper);
63
64
  if (!state) {
64
65
  return JNI_FALSE;
@@ -67,15 +68,44 @@ static jboolean updateFooterStateImmediate(
67
68
  auto concreteState =
68
69
  std::static_pointer_cast<const ConcreteState<TrueSheetFooterViewState>>(state);
69
70
 
70
- // Copy the latest data so fields this bridge doesn't set survive the update
71
- TrueSheetFooterViewState newState = concreteState->getData();
72
- newState.bottomInset = bottomInset;
73
- newState.initialized = true;
74
- concreteState->updateState(std::move(newState), EventQueue::UpdateMode::unstable_Immediate);
71
+ // Carries the translation too — RN's EventQueue drops a still-queued update
72
+ // for the same view (see updateFooterTranslation) when this one is enqueued
73
+ concreteState->updateState(
74
+ [bottomInset, translateY](const TrueSheetFooterViewState &oldData) -> StateData::Shared {
75
+ auto newData = oldData;
76
+ newData.bottomInset = bottomInset;
77
+ newData.translateY = translateY;
78
+ newData.initialized = true;
79
+ return std::make_shared<const TrueSheetFooterViewState>(newData);
80
+ },
81
+ EventQueue::UpdateMode::unstable_Immediate);
75
82
 
76
83
  return JNI_TRUE;
77
84
  }
78
85
 
86
+ // Mirrors the footer's resting translation into its state so measure()
87
+ // matches where it's drawn. Async — it only feeds measure().
88
+ static void updateFooterTranslation(
89
+ jni::alias_ref<jclass> /*clazz*/,
90
+ jni::alias_ref<jobject> stateWrapper,
91
+ jfloat translateY) {
92
+ auto state = getStateFromWrapper(stateWrapper);
93
+ if (!state) {
94
+ return;
95
+ }
96
+
97
+ auto concreteState =
98
+ std::static_pointer_cast<const ConcreteState<TrueSheetFooterViewState>>(state);
99
+
100
+ concreteState->updateState(
101
+ [translateY](const TrueSheetFooterViewState &oldData) -> StateData::Shared {
102
+ auto newData = oldData;
103
+ newData.translateY = translateY;
104
+ return std::make_shared<const TrueSheetFooterViewState>(newData);
105
+ },
106
+ EventQueue::UpdateMode::Asynchronous);
107
+ }
108
+
79
109
  // Publishes the precalculated bottom safe-area inset so a late-mounted
80
110
  // footer's first layout is already padded (see TrueSheetFooterViewShadowNode).
81
111
  static void setBottomSafeArea(jni::alias_ref<jclass> /*clazz*/, jfloat insetDp) {
@@ -90,6 +120,7 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void * /*reserved*/) {
90
120
  ->registerNatives({
91
121
  makeNativeMethod("nativeUpdateState", facebook::react::updateStateImmediate),
92
122
  makeNativeMethod("nativeUpdateFooterState", facebook::react::updateFooterStateImmediate),
123
+ makeNativeMethod("nativeUpdateFooterTranslation", facebook::react::updateFooterTranslation),
93
124
  makeNativeMethod("nativeSetBottomSafeArea", facebook::react::setBottomSafeArea),
94
125
  });
95
126
  });
@@ -59,4 +59,16 @@ void TrueSheetFooterViewShadowNode::adjustLayoutWithState() {
59
59
  }
60
60
  }
61
61
 
62
+ // Native moves the footer off its Yoga position without a layout pass. Folding
63
+ // that slide into the transform keeps measure() — and Pressability's press
64
+ // region — on the drawn footer.
65
+ Transform TrueSheetFooterViewShadowNode::getTransform() const {
66
+ auto transform = ConcreteViewShadowNode::getTransform();
67
+ auto translateY = getStateData().translateY;
68
+ if (translateY == 0) {
69
+ return transform;
70
+ }
71
+ return transform * Transform::Translate(0, translateY, 0);
72
+ }
73
+
62
74
  } // namespace facebook::react
@@ -20,6 +20,8 @@ class JSI_EXPORT TrueSheetFooterViewShadowNode final
20
20
 
21
21
  public:
22
22
  void adjustLayoutWithState();
23
+
24
+ Transform getTransform() const override;
23
25
  };
24
26
 
25
27
  } // namespace facebook::react
@@ -11,7 +11,8 @@ namespace facebook::react {
11
11
  /*
12
12
  * State for <TrueSheetFooterView> component.
13
13
  * Set from native with the sheet's bottom safe-area inset so the shadow node
14
- * pads the footer's bottom edge (see TrueSheetFooterViewShadowNode).
14
+ * pads the footer's bottom edge, and with the footer's native translation so
15
+ * measure() matches where it's drawn (see TrueSheetFooterViewShadowNode).
15
16
  */
16
17
  class TrueSheetFooterViewState final {
17
18
  public:
@@ -19,11 +20,16 @@ class TrueSheetFooterViewState final {
19
20
 
20
21
  #ifdef ANDROID
21
22
  TrueSheetFooterViewState(TrueSheetFooterViewState const &previousState, folly::dynamic data)
22
- : bottomInset(static_cast<float>(data["bottomInset"].getDouble())), initialized(true) {
23
+ : bottomInset(static_cast<float>(data["bottomInset"].getDouble())),
24
+ translateY(static_cast<float>(data["translateY"].getDouble())),
25
+ initialized(true) {
23
26
  }
24
27
  #endif
25
28
 
26
29
  float bottomInset{0};
30
+ // Native slide off the Yoga position — above the keyboard, or riding the
31
+ // sheet below its lowest detent (Android)
32
+ float translateY{0};
27
33
  // True once native has pushed a real inset — until then the shadow node
28
34
  // seeds the layout from TrueSheetInsets (see TrueSheetFooterViewShadowNode)
29
35
  bool initialized{false};
@@ -67,7 +67,7 @@ using namespace facebook::react;
67
67
  return;
68
68
  }
69
69
  _bottomInset = bottomInset;
70
- [self pushBottomInsetState];
70
+ [self pushStateWithMode:EventQueue::UpdateMode::unstable_Immediate];
71
71
  }
72
72
 
73
73
  // Skipped while the keyboard is open — an absolute footer rises above the
@@ -77,22 +77,36 @@ using namespace facebook::react;
77
77
  return;
78
78
  }
79
79
  _keyboardVisible = keyboardVisible;
80
- [self pushBottomInsetState];
80
+ [self pushStateWithMode:EventQueue::UpdateMode::unstable_Immediate];
81
81
  }
82
82
 
83
- - (void)pushBottomInsetState {
83
+ // The keyboard slide is a transform the shadow tree can't see — mirror it so
84
+ // measure() (and Pressability's press region) match where the footer is drawn.
85
+ // Async: no layout reads it.
86
+ - (void)setKeyboardOffset:(CGFloat)keyboardOffset {
87
+ if (_currentKeyboardOffset == keyboardOffset) {
88
+ return;
89
+ }
90
+ _currentKeyboardOffset = keyboardOffset;
91
+ [self pushStateWithMode:EventQueue::UpdateMode::Asynchronous];
92
+ }
93
+
94
+ // Every push carries all fields — RN's EventQueue drops a still-queued update
95
+ // for the same view when the next one is enqueued.
96
+ - (void)pushStateWithMode:(EventQueue::UpdateMode)mode {
84
97
  if (!_state) {
85
98
  return;
86
99
  }
87
100
 
88
101
  TrueSheetFooterViewState newState;
89
102
  newState.bottomInset = _keyboardVisible ? 0 : _bottomInset;
103
+ newState.translateY = -_currentKeyboardOffset;
90
104
  newState.initialized = true;
91
- // Immediate so the inset lands before anything reads the footer's height —
92
- // the async path races the keyboard caret scroll and detent setup, leaving
93
- // them an inset stale (same reason Android bridges through
94
- // TrueSheetStateUpdater).
95
- _state->updateState(std::move(newState), EventQueue::UpdateMode::unstable_Immediate);
105
+ // Inset changes go Immediate so they land before anything reads the
106
+ // footer's height — the async path races the keyboard caret scroll and
107
+ // detent setup, leaving them an inset stale (same reason Android bridges
108
+ // through TrueSheetStateUpdater).
109
+ _state->updateState(std::move(newState), mode);
96
110
  _didPushBottomInset = YES;
97
111
  }
98
112
 
@@ -215,7 +229,7 @@ using namespace facebook::react;
215
229
  [self setKeyboardVisible:YES];
216
230
 
217
231
  CGFloat slide = [self keyboardSlideForHeight:height];
218
- _currentKeyboardOffset = slide;
232
+ [self setKeyboardOffset:slide];
219
233
 
220
234
  [UIView animateWithDuration:duration
221
235
  delay:0
@@ -230,8 +244,9 @@ using namespace facebook::react;
230
244
  }
231
245
 
232
246
  - (void)keyboardWillHide:(NSTimeInterval)duration curve:(UIViewAnimationOptions)curve {
247
+ // Offset first — the visible push replaces it in the queue, one commit
248
+ [self setKeyboardOffset:0];
233
249
  [self setKeyboardVisible:NO];
234
- _currentKeyboardOffset = 0;
235
250
 
236
251
  [UIView animateWithDuration:duration
237
252
  delay:0
@@ -266,8 +281,8 @@ using namespace facebook::react;
266
281
 
267
282
  // avoidKeyboard flipped off mid-keyboard — drop the slide already applied
268
283
  if (![self floatsAboveKeyboard]) {
284
+ [self setKeyboardOffset:0];
269
285
  [self setKeyboardVisible:NO];
270
- _currentKeyboardOffset = 0;
271
286
  self.transform = CGAffineTransformIdentity;
272
287
  return;
273
288
  }
@@ -275,7 +290,7 @@ using namespace facebook::react;
275
290
  [self setKeyboardVisible:YES];
276
291
 
277
292
  CGFloat slide = [self keyboardSlideForHeight:height];
278
- _currentKeyboardOffset = slide;
293
+ [self setKeyboardOffset:slide];
279
294
  self.transform = CGAffineTransformMakeTranslation(0, -slide);
280
295
  }
281
296
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodev09/react-native-true-sheet",
3
- "version": "4.0.0-beta.26",
3
+ "version": "4.0.0-beta.27",
4
4
  "description": "The true native bottom sheet experience for your React Native Apps.",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/module/index.js",