Skip to content

Commit a508e94

Browse files
authored
feat: add dismissAll static method (#393)
* fix(android): keyboard and focus handling inside RN Modal - Use targetView.rootView.findFocus() instead of activity.currentFocus in keyboard observer - Use delegate.findRootContainerView().findFocus() for saveFocusedView - Add findRootContainerView to TrueSheetViewControllerDelegate interface * feat: add dismissAll static method * fix(ios): dismiss all sheets without dismissing external view controllers * fix(android): dismiss all sheets without dismissing sheets behind modal * docs: add dismissAll method documentation * fix(android): prevent translation of sheets hidden by modal * feat(web): add dismissAll method * feat(web): add static methods for present, dismiss, resize
1 parent 2c0ca2d commit a508e94

19 files changed

Lines changed: 271 additions & 6 deletions

android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,31 @@ class TrueSheetModule(reactContext: ReactApplicationContext) :
9595
}
9696
}
9797

98+
/**
99+
* Dismiss all presented sheets by dismissing from the bottom of the stack
100+
*
101+
* @param animated Whether to animate the dismissals
102+
* @param promise Promise that resolves when all sheets are dismissed
103+
*/
104+
@ReactMethod
105+
fun dismissAll(animated: Boolean, promise: Promise) {
106+
Handler(Looper.getMainLooper()).post {
107+
try {
108+
val rootSheet = TrueSheetStackManager.getRootSheet()
109+
if (rootSheet == null) {
110+
promise.resolve(null)
111+
return@post
112+
}
113+
114+
rootSheet.dismissAll(animated) {
115+
promise.resolve(null)
116+
}
117+
} catch (e: Exception) {
118+
promise.reject("OPERATION_FAILED", "Failed to dismiss all sheets: ${e.message}", e)
119+
}
120+
}
121+
}
122+
98123
/**
99124
* Helper method to get TrueSheetView by tag and execute closure
100125
*/

android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,16 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
310310
viewController.dismiss(animated)
311311
}
312312

313+
@UiThread
314+
fun dismissAll(animated: Boolean = true, promiseCallback: () -> Unit) {
315+
// Dismiss all sheets above first
316+
dismiss(animated) {}
317+
318+
// Then dismiss itself
319+
viewController.dismissPromise = promiseCallback
320+
viewController.dismiss(animated)
321+
}
322+
313323
@UiThread
314324
fun resize(detentIndex: Int, promiseCallback: () -> Unit) {
315325
if (!viewController.isPresented) {

android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
131131

132132
private var interactionState: InteractionState = InteractionState.Idle
133133
private var isDismissing = false
134-
private var wasHiddenByModal = false
134+
internal var wasHiddenByModal = false
135135
private var shouldAnimatePresent = false
136136
private var isPresentAnimating = false
137137

android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ object TrueSheetStackManager {
2222
}
2323

2424
/**
25-
* Returns the topmost presented and visible sheet.
25+
* Returns the topmost presented and visible sheet that is not hidden by modal.
2626
* Must be called within synchronized block.
2727
*/
2828
private fun findTopmostSheet(): TrueSheetView? =
2929
presentedSheetStack.lastOrNull {
30-
it.viewController.isPresented && it.viewController.isSheetVisible
30+
it.viewController.isPresented && it.viewController.isSheetVisible && !it.viewController.wasHiddenByModal
3131
}
3232

3333
/**
@@ -151,4 +151,27 @@ object TrueSheetStackManager {
151151
return findTopmostSheet()
152152
}
153153
}
154+
155+
/**
156+
* Returns the root presented sheet for dismissAll.
157+
* Starts from the topmost sheet and walks up to find the root of its stack.
158+
* Stops at modal boundary (parent hidden by modal) or when there's no parent.
159+
*/
160+
@JvmStatic
161+
fun getRootSheet(): TrueSheetView? {
162+
synchronized(presentedSheetStack) {
163+
val topmost = presentedSheetStack.lastOrNull { it.viewController.isPresented } ?: return null
164+
165+
var current: TrueSheetView = topmost
166+
while (true) {
167+
val parent = current.viewController.parentSheetView ?: return current
168+
169+
if (parent.viewController.wasHiddenByModal) {
170+
return current
171+
}
172+
173+
current = parent
174+
}
175+
}
176+
}
154177
}

docs/docs/guides/stacking.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ await sheet3.current?.dismiss() // Dismisses Sheet 3, Sheet 1 and Sheet 2 stay
5252

5353
This is useful when you need to dismiss all child sheets and return focus to a parent sheet without closing it.
5454

55+
### Dismiss All Sheets
56+
57+
To dismiss all sheets in the current stack at once, use the static [`dismissAll()`](/reference/methods#dismissall) method:
58+
59+
```tsx
60+
await TrueSheet.dismissAll()
61+
```
62+
63+
:::info Presentation Context
64+
This only dismisses sheets in the current presentation context. Sheets presented behind a modal (e.g., React Navigation modal or React Native Modal) will not be affected.
65+
:::
66+
5567
## Stack Behavior (Web)
5668

5769
On web, you can customize the stacking behavior using the [`stackBehavior`](https://gorhom.dev/react-native-bottom-sheet/modal/props#stackbehavior) prop:

docs/docs/reference/03-methods.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,22 @@ await TrueSheet.dismiss('my-sheet', false)
103103
// Resize to 80%
104104
await TrueSheet.resize('my-sheet', 0.8)
105105
```
106+
107+
### `dismissAll`
108+
109+
Dismisses all presented sheets in the current stack. This is useful when you need to close all sheets at once, for example when navigating away or resetting the UI state.
110+
111+
| Parameters | Required | Default |
112+
| - | - | - |
113+
| `animated: boolean` | No | `true` |
114+
115+
```tsx
116+
await TrueSheet.dismissAll()
117+
118+
// Dismiss all without animation
119+
await TrueSheet.dismissAll(false)
120+
```
121+
122+
:::note
123+
This only dismisses sheets in the current presentation context. Sheets presented behind a modal (e.g., React Navigation modal or React Native Modal) will not be affected.
124+
:::

example/shared/src/components/sheets/BasicSheet.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ export const BasicSheet = forwardRef((props: BasicSheetProps, ref: Ref<TrueSheet
124124
<DemoContent color={DARK_BLUE} />
125125
<DemoContent color={DARK_BLUE} />
126126
<DemoContent color={DARK_BLUE} />
127+
<Button text="Dismiss All" onPress={() => TrueSheet.dismissAll()} />
128+
<Button text="Dismiss Stack" onPress={() => TrueSheet.dismiss('main')} />
127129
<Button text="Close" onPress={() => childSheet.current?.dismiss()} />
128130
</TrueSheet>
129131
</TrueSheet>

example/shared/src/screens/ModalScreen.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { TrueSheet, TrueSheetProvider } from '@lodev09/react-native-true-sheet';
44

55
import { BLUE, DARK_GRAY, GAP, LIGHT_GRAY, SPACING } from '../utils';
66
import { Button, Input, Spacer } from '../components';
7-
import { PromptSheet, FlatListSheet, ScrollViewSheet } from '../components/sheets';
7+
import { PromptSheet, FlatListSheet, ScrollViewSheet, BasicSheet } from '../components/sheets';
88

99
export interface ModalScreenProps {
1010
onNavigateToTest?: () => void;
@@ -15,7 +15,10 @@ export const ModalScreen = ({ onNavigateToTest, onDismiss }: ModalScreenProps) =
1515
const promptSheet = useRef<TrueSheet>(null);
1616
const flatlistSheet = useRef<TrueSheet>(null);
1717

18+
const basicSheet = useRef<TrueSheet>(null);
19+
1820
const [modalVisible, setModalVisible] = useState(false);
21+
const modalBasicSheet = useRef<TrueSheet>(null);
1922
const modalPromptSheet = useRef<TrueSheet>(null);
2023
const modalFlatlistSheet = useRef<TrueSheet>(null);
2124
const modalScrollViewSheet = useRef<TrueSheet>(null);
@@ -37,12 +40,14 @@ export const ModalScreen = ({ onNavigateToTest, onDismiss }: ModalScreenProps) =
3740
</View>
3841
<Input />
3942
<Button text="Dismiss Modal" onPress={onDismiss} />
43+
<Button text="TrueSheet Basic" onPress={() => basicSheet.current?.present()} />
4044
<Button text="TrueSheet Prompt" onPress={() => promptSheet.current?.present()} />
4145
<Button text="TrueSheet FlatList" onPress={() => flatlistSheet.current?.present()} />
4246
<Button text="Open RN Modal" onPress={() => setModalVisible(true)} />
4347
<Spacer />
4448
<Button text="Navigate Test" onPress={onNavigateToTest} />
4549

50+
<BasicSheet ref={basicSheet} />
4651
<PromptSheet ref={promptSheet} />
4752
<FlatListSheet ref={flatlistSheet} />
4853

@@ -59,6 +64,7 @@ export const ModalScreen = ({ onNavigateToTest, onDismiss }: ModalScreenProps) =
5964
This is a React Native Modal. You can present TrueSheets from here!
6065
</Text>
6166
</View>
67+
<Button text="Basic Sheet" onPress={() => modalBasicSheet.current?.present()} />
6268
<Button text="Prompt Sheet" onPress={() => modalPromptSheet.current?.present()} />
6369
<Button
6470
text="ScrollView Sheet"
@@ -68,6 +74,7 @@ export const ModalScreen = ({ onNavigateToTest, onDismiss }: ModalScreenProps) =
6874
<Spacer />
6975
<Button text="Close Modal" onPress={() => setModalVisible(false)} />
7076

77+
<BasicSheet ref={modalBasicSheet} />
7178
<PromptSheet ref={modalPromptSheet} />
7279
<FlatListSheet ref={modalFlatlistSheet} />
7380
<ScrollViewSheet ref={modalScrollViewSheet} />

ios/TrueSheetModule.mm

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#import "TrueSheetModule.h"
1515
#import <React/RCTUtils.h>
1616
#import "TrueSheetView.h"
17+
#import "TrueSheetViewController.h"
1718

1819
#import <TrueSheetSpec/TrueSheetSpec.h>
1920

@@ -115,6 +116,46 @@ - (void)resizeByRef:(double)viewTag
115116
});
116117
}
117118

119+
- (void)dismissAll:(BOOL)animated
120+
resolve:(RCTPromiseResolveBlock)resolve
121+
reject:(RCTPromiseRejectBlock)reject {
122+
RCTExecuteOnMainQueue(^{
123+
@synchronized(viewRegistry) {
124+
// Find the root presented sheet (one without a parent TrueSheet)
125+
TrueSheetView *rootSheet = nil;
126+
127+
for (TrueSheetView *view in viewRegistry.allValues) {
128+
if (!view.viewController.isPresented) {
129+
continue;
130+
}
131+
132+
UIViewController *presenter = view.viewController.presentingViewController;
133+
BOOL hasParentSheet = [presenter isKindOfClass:[TrueSheetViewController class]];
134+
135+
if (!hasParentSheet) {
136+
rootSheet = view;
137+
break;
138+
}
139+
}
140+
141+
if (!rootSheet) {
142+
resolve(nil);
143+
return;
144+
}
145+
146+
[rootSheet dismissAllAnimated:animated
147+
completion:^(BOOL success, NSError *_Nullable error) {
148+
if (success) {
149+
resolve(nil);
150+
} else {
151+
reject(@"DISMISS_FAILED", error.localizedDescription ?: @"Failed to dismiss sheets",
152+
error);
153+
}
154+
}];
155+
}
156+
});
157+
}
158+
118159
#pragma mark - Helper Methods
119160

120161
+ (nullable TrueSheetView *)getTrueSheetViewByTag:(NSNumber *)reactTag {

ios/TrueSheetView.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ typedef void (^TrueSheetCompletionBlock)(BOOL success, NSError *_Nullable error)
2323

2424
@interface TrueSheetView : RCTViewComponentView
2525

26+
@property (nonatomic, readonly) TrueSheetViewController *viewController;
27+
2628
// TurboModule methods
2729
- (void)presentAtIndex:(NSInteger)index
2830
animated:(BOOL)animated
@@ -32,6 +34,8 @@ typedef void (^TrueSheetCompletionBlock)(BOOL success, NSError *_Nullable error)
3234

3335
- (void)resizeToIndex:(NSInteger)index completion:(nullable TrueSheetCompletionBlock)completion;
3436

37+
- (void)dismissAllAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionBlock)completion;
38+
3539
@end
3640

3741
NS_ASSUME_NONNULL_END

0 commit comments

Comments
 (0)