@lodev09/react-native-true-sheet 3.11.13 → 3.11.15
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/lodev09/truesheet/TrueSheetView.kt +13 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +1 -1
- package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +14 -1
- package/ios/TrueSheetView.mm +0 -1
- package/ios/TrueSheetViewController.mm +65 -3
- package/ios/core/TrueSheetGrabberView.mm +13 -3
- package/package.json +1 -1
|
@@ -366,11 +366,22 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
366
366
|
val parentSheet = TrueSheetStackManager.getTopmostSheet()
|
|
367
367
|
val isFocusedViewWithinSheet = parentSheet?.viewController?.isFocusedViewWithinSheet() == true
|
|
368
368
|
val shouldDismissKeyboard = isFocusedViewWithinSheet || viewController.isDimmedAtDetentIndex(detentIndex)
|
|
369
|
-
if (KeyboardUtils.isKeyboardVisible(reactContext)
|
|
369
|
+
if (shouldDismissKeyboard && KeyboardUtils.isKeyboardVisible(reactContext)) {
|
|
370
370
|
viewController.saveFocusedView()
|
|
371
371
|
KeyboardUtils.dismiss(this) {
|
|
372
|
-
post {
|
|
372
|
+
post { presentSheet(detentIndex, animated, promiseCallback) }
|
|
373
373
|
}
|
|
374
|
+
// Blur like iOS so JS focus state stays in sync until restoreFocusedView() refocuses it
|
|
375
|
+
rootView.findFocus()?.clearFocus()
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
presentSheet(detentIndex, animated, promiseCallback)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private fun presentSheet(detentIndex: Int, animated: Boolean, promiseCallback: () -> Unit) {
|
|
383
|
+
if (viewController.isPresented) {
|
|
384
|
+
promiseCallback()
|
|
374
385
|
return
|
|
375
386
|
}
|
|
376
387
|
|
|
@@ -1050,7 +1050,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
1050
1050
|
if (!viewToFocus.isAttachedToWindow) return
|
|
1051
1051
|
if (viewToFocus.requestFocus()) {
|
|
1052
1052
|
viewToFocus.postDelayed({
|
|
1053
|
-
KeyboardUtils.show(viewToFocus)
|
|
1053
|
+
if (viewToFocus.isFocused) KeyboardUtils.show(viewToFocus)
|
|
1054
1054
|
}, 100)
|
|
1055
1055
|
}
|
|
1056
1056
|
}
|
|
@@ -3,6 +3,7 @@ package com.lodev09.truesheet.utils
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.os.Build
|
|
5
5
|
import android.view.View
|
|
6
|
+
import android.view.WindowInsets
|
|
6
7
|
import android.view.inputmethod.InputMethodManager
|
|
7
8
|
import androidx.core.view.ViewCompat
|
|
8
9
|
import androidx.core.view.WindowInsetsAnimationCompat
|
|
@@ -36,10 +37,22 @@ object KeyboardUtils {
|
|
|
36
37
|
* Dismisses the soft keyboard with an optional callback when the animation completes.
|
|
37
38
|
*/
|
|
38
39
|
fun dismiss(view: View, onComplete: (() -> Unit)?) {
|
|
40
|
+
if (!isKeyboardVisible(view)) {
|
|
41
|
+
onComplete?.invoke()
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
40
46
|
val focusedView = view.rootView.findFocus()
|
|
41
47
|
|
|
42
|
-
|
|
48
|
+
// A keyboard can stay up with nothing focused, e.g. a blur that lands after focus was restored,
|
|
49
|
+
// so hide it through the window instead of treating it as already hidden.
|
|
50
|
+
if (focusedView == null) {
|
|
51
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
52
|
+
view.windowInsetsController?.hide(WindowInsets.Type.ime())
|
|
53
|
+
} else {
|
|
54
|
+
imm?.hideSoftInputFromWindow(view.windowToken, 0)
|
|
55
|
+
}
|
|
43
56
|
onComplete?.invoke()
|
|
44
57
|
return
|
|
45
58
|
}
|
package/ios/TrueSheetView.mm
CHANGED
|
@@ -382,7 +382,6 @@ using namespace facebook::react;
|
|
|
382
382
|
[_controller.view addSubview:_containerView];
|
|
383
383
|
[LayoutUtil pinView:_containerView toParentView:_controller.view edges:UIRectEdgeAll];
|
|
384
384
|
[_controller.view bringSubviewToFront:_containerView];
|
|
385
|
-
_containerView.accessibilityViewIsModal = YES;
|
|
386
385
|
_controller.accessibilityContentView = _containerView;
|
|
387
386
|
[_controller setupAccessibilityContainer];
|
|
388
387
|
|
|
@@ -43,6 +43,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
43
43
|
- (void)restoreWindowAccessibilityElements;
|
|
44
44
|
- (void)setSheetAccessibilityElementsHidden:(BOOL)hidden;
|
|
45
45
|
- (void)setAccessibilityContentElement:(UIView *)contentView;
|
|
46
|
+
- (NSArray *)accessibilityGrabberElements;
|
|
46
47
|
- (void)endInteractiveDismissState;
|
|
47
48
|
- (void)emitInteractivePosition;
|
|
48
49
|
- (void)animateInteractiveContainerToTransform:(CGAffineTransform)transform
|
|
@@ -52,6 +53,20 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
52
53
|
|
|
53
54
|
@end
|
|
54
55
|
|
|
56
|
+
// VoiceOver delivers the two-finger Z (escape) gesture by walking up the view
|
|
57
|
+
// hierarchy from the focused element, so the sheet's root view handles it.
|
|
58
|
+
@interface TrueSheetControllerView : UIView
|
|
59
|
+
@property (nonatomic, weak) TrueSheetViewController *controller;
|
|
60
|
+
@end
|
|
61
|
+
|
|
62
|
+
@implementation TrueSheetControllerView
|
|
63
|
+
|
|
64
|
+
- (BOOL)accessibilityPerformEscape {
|
|
65
|
+
return [self.controller accessibilityPerformEscape];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@end
|
|
69
|
+
|
|
55
70
|
@implementation TrueSheetViewController {
|
|
56
71
|
TrueSheetPositionState _lastEmittedPositionState;
|
|
57
72
|
CGFloat _lastWidth;
|
|
@@ -214,6 +229,12 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
214
229
|
|
|
215
230
|
#pragma mark - View Lifecycle
|
|
216
231
|
|
|
232
|
+
- (void)loadView {
|
|
233
|
+
TrueSheetControllerView *view = [[TrueSheetControllerView alloc] init];
|
|
234
|
+
view.controller = self;
|
|
235
|
+
self.view = view;
|
|
236
|
+
}
|
|
237
|
+
|
|
217
238
|
- (void)viewDidLoad {
|
|
218
239
|
[super viewDidLoad];
|
|
219
240
|
self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
|
|
@@ -321,6 +342,32 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
321
342
|
}
|
|
322
343
|
}
|
|
323
344
|
|
|
345
|
+
- (NSArray *)accessibilityGrabberElements {
|
|
346
|
+
if (_grabberView && !_grabberView.hidden) {
|
|
347
|
+
return @[ _grabberView ];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// The system grabber is an accessibility element UIKit adds to the presented view
|
|
351
|
+
// (a sibling of our root view). Expose it so VoiceOver can resize or dismiss with it.
|
|
352
|
+
// UIKit also keeps a transparent bottom grabber there, so skip invisible views.
|
|
353
|
+
NSMutableArray *elements = [NSMutableArray array];
|
|
354
|
+
for (UIView *subview in self.presentationController.presentedView.subviews) {
|
|
355
|
+
if (subview != self.view && subview.isAccessibilityElement && !subview.hidden && subview.alpha > 0) {
|
|
356
|
+
[elements addObject:subview];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return elements;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
- (BOOL)accessibilityPerformEscape {
|
|
363
|
+
if (!_isPresented || !self.dismissible) {
|
|
364
|
+
return NO;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
|
368
|
+
return YES;
|
|
369
|
+
}
|
|
370
|
+
|
|
324
371
|
- (void)setAccessibilityContentElement:(UIView *)contentView {
|
|
325
372
|
BOOL hasAccessibilityFooterElements = [contentView isKindOfClass:[TrueSheetContainerView class]] &&
|
|
326
373
|
[(TrueSheetContainerView *)contentView hasAccessibilityFooterElements];
|
|
@@ -333,12 +380,12 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
333
380
|
// its accessibilityElements getter recomputes live, so a footer/content subtree
|
|
334
381
|
// that remounts (e.g. swapping a footer button on a state change) stays
|
|
335
382
|
// discoverable instead of leaving this snapshot pointing at destroyed views.
|
|
336
|
-
|
|
383
|
+
NSMutableArray *accessibilityElements = [[self accessibilityGrabberElements] mutableCopy];
|
|
337
384
|
if (isAccessibilityModal) {
|
|
338
385
|
NSArray *contentElements = contentView.accessibilityElements;
|
|
339
|
-
accessibilityElements
|
|
386
|
+
[accessibilityElements addObjectsFromArray:contentElements.count > 0 ? contentElements : @[ contentView ]];
|
|
340
387
|
} else {
|
|
341
|
-
accessibilityElements
|
|
388
|
+
[accessibilityElements addObject:contentView];
|
|
342
389
|
}
|
|
343
390
|
|
|
344
391
|
self.view.isAccessibilityElement = NO;
|
|
@@ -351,6 +398,15 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
351
398
|
presentedView.accessibilityViewIsModal = isAccessibilityModal;
|
|
352
399
|
presentedView.accessibilityElements = accessibilityElements;
|
|
353
400
|
|
|
401
|
+
// The system grabber is a sibling of our root view, so an escape performed while it
|
|
402
|
+
// is focused walks presentedView → window and never reaches TrueSheetControllerView.
|
|
403
|
+
if (@available(iOS 17.0, *)) {
|
|
404
|
+
__weak __typeof(self) weakSelf = self;
|
|
405
|
+
presentedView.accessibilityPerformEscapeBlock = ^BOOL {
|
|
406
|
+
return [weakSelf accessibilityPerformEscape];
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
354
410
|
UIWindow *window = self.view.window;
|
|
355
411
|
UIViewController *presentingViewController = [self accessibilityPresentingViewController];
|
|
356
412
|
if (window && presentingViewController.view) {
|
|
@@ -1147,6 +1203,12 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
1147
1203
|
_grabberView.onIncrement = nil;
|
|
1148
1204
|
_grabberView.onDecrement = nil;
|
|
1149
1205
|
}
|
|
1206
|
+
|
|
1207
|
+
// Only the topmost sheet may refresh, or a parent's prop update would steal the
|
|
1208
|
+
// window accessibility override from the child presented over it.
|
|
1209
|
+
if (_isPresented && self.isTopmostPresentedController) {
|
|
1210
|
+
[self setupAccessibilityContainer];
|
|
1211
|
+
}
|
|
1150
1212
|
}
|
|
1151
1213
|
|
|
1152
1214
|
- (void)handleGrabberTap {
|
|
@@ -91,6 +91,13 @@ static const CGFloat kHitPaddingVertical = 10.0;
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// Pad the pill evenly like the system grabber. The view frame itself stays inside
|
|
95
|
+
// the sheet, since the reveal check reads the frame rather than this rect.
|
|
96
|
+
- (CGRect)accessibilityFrame {
|
|
97
|
+
CGRect rect = CGRectInset(_vibrancyView.frame, -kHitPaddingHorizontal, -kHitPaddingVertical);
|
|
98
|
+
return UIAccessibilityConvertFrameToScreenCoordinates(rect, self);
|
|
99
|
+
}
|
|
100
|
+
|
|
94
101
|
- (void)accessibilityIncrement {
|
|
95
102
|
if (_onIncrement) {
|
|
96
103
|
_onIncrement();
|
|
@@ -144,14 +151,17 @@ static const CGFloat kHitPaddingVertical = 10.0;
|
|
|
144
151
|
CGFloat topMargin = [self effectiveTopMargin];
|
|
145
152
|
CGFloat parentWidth = self.superview ? self.superview.bounds.size.width : UIScreen.mainScreen.bounds.size.width;
|
|
146
153
|
|
|
154
|
+
// Keep the frame inside the sheet. A frame above the top edge reads as a clipped element
|
|
155
|
+
// to VoiceOver, and the sheet reveals it on focus by moving to the next detent.
|
|
147
156
|
CGFloat frameWidth = pillWidth + kHitPaddingHorizontal * 2;
|
|
148
|
-
CGFloat
|
|
149
|
-
CGFloat
|
|
157
|
+
CGFloat frameY = MAX(0, topMargin - kHitPaddingVertical);
|
|
158
|
+
CGFloat pillY = topMargin - frameY;
|
|
159
|
+
CGFloat frameHeight = pillY + pillHeight + kHitPaddingVertical;
|
|
150
160
|
|
|
151
161
|
self.frame = CGRectMake((parentWidth - frameWidth) / 2.0, frameY, frameWidth, frameHeight);
|
|
152
162
|
self.backgroundColor = UIColor.clearColor;
|
|
153
163
|
|
|
154
|
-
CGRect pillRect = CGRectMake(kHitPaddingHorizontal,
|
|
164
|
+
CGRect pillRect = CGRectMake(kHitPaddingHorizontal, pillY, pillWidth, pillHeight);
|
|
155
165
|
_vibrancyView.frame = pillRect;
|
|
156
166
|
_vibrancyView.layer.cornerRadius = [self effectiveCornerRadius];
|
|
157
167
|
_vibrancyView.clipsToBounds = YES;
|
package/package.json
CHANGED