Skip to content

Commit cb99658

Browse files
committed
feat: add modal detents prop
1 parent e8fcde5 commit cb99658

File tree

6 files changed

+141
-4
lines changed

6 files changed

+141
-4
lines changed

packages/react-native/Libraries/Modal/Modal.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ export interface ModalPropsIOS {
6363
| 'overFullScreen'
6464
| undefined;
6565

66+
/**
67+
* The `detents` determines the height for modal,
68+
* based on the detents value.
69+
* It can be a combination of `large`, `medium` or
70+
* any number.
71+
* for eg: ['140', 'medium', 'large']
72+
*/
73+
detents?: Array<string> | undefined;
74+
6675
/**
6776
* The `supportedOrientations` prop allows the modal to be rotated to any of the specified orientations.
6877
* On iOS, the modal is still restricted by what's specified in your app's Info.plist's UISupportedInterfaceOrientations field.

packages/react-native/Libraries/Modal/Modal.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,15 @@ export type ModalPropsIOS = {
121121
| 'overFullScreen'
122122
),
123123

124+
/**
125+
* The `detents` determines the height for modal,
126+
* based on the detents value.
127+
* It can be a combination of `large`, `medium` or
128+
* any number.
129+
* for eg: ['140', 'medium', 'large']
130+
*/
131+
detents?: ?$ReadOnlyArray<string>,
132+
124133
/**
125134
* The `supportedOrientations` prop allows the modal to be rotated to any of the specified orientations.
126135
* On iOS, the modal is still restricted by what's specified in your app's Info.plist's UISupportedInterfaceOrientations field.
@@ -208,6 +217,27 @@ function confirmProps(props: ModalProps) {
208217
'Modal requires the onRequestClose prop when used with `allowSwipeDismissal`. This is necessary to prevent state corruption.',
209218
);
210219
}
220+
221+
if (
222+
Platform.OS === 'ios' &&
223+
props.detents &&
224+
!props.detents.every(detent => typeof detent === 'string')
225+
) {
226+
console.warn(
227+
'Modal detents should be a combination of `large`, `medium` or any number represented as a string.',
228+
);
229+
}
230+
231+
if (
232+
Platform.OS === 'ios' &&
233+
props.detents?.length > 0 &&
234+
props.presentationStyle !== 'pageSheet' &&
235+
props.presentationStyle !== 'formSheet'
236+
) {
237+
console.warn(
238+
'Modal detents are only supported with `pageSheet` or `formSheet` presentation styles.',
239+
);
240+
}
211241
}
212242
}
213243

@@ -344,6 +374,7 @@ class Modal extends React.Component<ModalProps, ModalState> {
344374
supportedOrientations={this.props.supportedOrientations}
345375
onOrientationChange={this.props.onOrientationChange}
346376
allowSwipeDismissal={this.props.allowSwipeDismissal}
377+
detents={this.props.detents}
347378
testID={this.props.testID}>
348379
<VirtualizedListContextResetter>
349380
<ScrollView.Context.Provider value={null}>

packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ @implementation RCTModalHostViewComponentView {
107107
BOOL _shouldPresent;
108108
BOOL _isPresented;
109109
BOOL _modalInPresentation;
110+
NSArray<UISheetPresentationControllerDetent *>* _detents;
110111
}
111112

112113
- (instancetype)initWithFrame:(CGRect)frame
@@ -156,6 +157,13 @@ - (void)ensurePresentedOnlyIfNeeded
156157
self.viewController.presentationController.delegate = self;
157158
self.viewController.modalInPresentation = _modalInPresentation;
158159

160+
UISheetPresentationController *sheetPresentationController =
161+
_viewController.sheetPresentationController;
162+
163+
if (sheetPresentationController) {
164+
sheetPresentationController.detents = _detents;
165+
}
166+
159167
_isPresented = YES;
160168
[self presentViewController:self.viewController
161169
animated:_shouldAnimatePresentation
@@ -243,7 +251,7 @@ - (void)boundsDidChange:(CGRect)newBounds
243251

244252
if (_state != nullptr) {
245253
auto newState = ModalHostViewState{RCTSizeFromCGSize(newBounds.size)};
246-
_state->updateState(std::move(newState));
254+
_state->updateState(std::move(newState), EventQueue::UpdateMode::unstable_Immediate);
247255
}
248256
}
249257

@@ -282,6 +290,13 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
282290
_modalInPresentation = !newProps.allowSwipeDismissal;
283291
self.viewController.modalInPresentation = _modalInPresentation;
284292
}
293+
294+
if (oldViewProps.detents != newProps.detents) {
295+
_detents = [self calculateDetents:newProps.detents];
296+
if (_viewController.sheetPresentationController) {
297+
_viewController.sheetPresentationController.detents = _detents;
298+
}
299+
}
285300

286301
_shouldPresent = newProps.visible;
287302
[self ensurePresentedOnlyIfNeeded];
@@ -325,6 +340,41 @@ - (void)presentationControllerDidDismiss:(UIPresentationController *)presentatio
325340
}
326341
}
327342

343+
#pragma mark - Helpers
344+
345+
- (NSArray<UISheetPresentationControllerDetent *> *)calculateDetents:(const std::vector<std::string> &)detents {
346+
if (@available(iOS 16.0, *)) {
347+
NSMutableArray<UISheetPresentationControllerDetent *> *detentsArray = [NSMutableArray new];
348+
349+
for (const auto &detent : detents) {
350+
NSString *detentString = [NSString stringWithUTF8String:detent.c_str()];
351+
352+
if ([detentString isEqualToString:@"medium"]) {
353+
[detentsArray addObject:[UISheetPresentationControllerDetent mediumDetent]];
354+
} else if ([detentString isEqualToString:@"large"]) {
355+
[detentsArray addObject:[UISheetPresentationControllerDetent largeDetent]];
356+
} else {
357+
// Try to parse as a number (custom detent)
358+
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
359+
NSNumber *number = [formatter numberFromString:detentString];
360+
361+
if (number != nil) {
362+
float value = [number floatValue];
363+
[detentsArray addObject:[UISheetPresentationControllerDetent
364+
customDetentWithIdentifier:nil
365+
resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
366+
return value;
367+
}]];
368+
}
369+
}
370+
}
371+
372+
return detentsArray;
373+
}
374+
375+
return [NSMutableArray new];
376+
}
377+
328378
@end
329379

330380
#ifdef __cplusplus

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ internal class ReactModalHostManager :
6969
// iOS only
7070
}
7171

72+
@ReactProp(name = "detent")
73+
override fun setDetent(view: ReactModalHostView, value: String?) {
74+
// iOS only
75+
}
76+
7277
@ReactProp(name = "presentationStyle")
7378
override fun setPresentationStyle(view: ReactModalHostView, value: String?): Unit = Unit
7479

packages/react-native/src/private/specs_DEPRECATED/components/RCTModalHostViewNativeComponent.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ type RCTModalHostViewNativeProps = $ReadOnly<{
4242
'fullScreen',
4343
>,
4444

45+
/**
46+
* The `detents` determines the height for modal,
47+
* based on the detents value.
48+
* It can be a combination of `large`, `medium` or
49+
* any number.
50+
* for eg: ['140', 'medium', 'large']
51+
*/
52+
detents?: ?$ReadOnlyArray<string>,
53+
4554
/**
4655
* The `transparent` prop determines whether your modal will fill the
4756
* entire view.

packages/rn-tester/js/examples/Modal/ModalPresentation.js

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,15 @@ import {RNTesterThemeContext} from '../../components/RNTesterTheme';
1919
import RNTOption from '../../components/RNTOption';
2020
import * as React from 'react';
2121
import {useCallback, useContext, useState} from 'react';
22-
import {Modal, Platform, StyleSheet, Switch, Text, View} from 'react-native';
22+
import {
23+
Modal,
24+
Platform,
25+
ScrollView,
26+
StyleSheet,
27+
Switch,
28+
Text,
29+
View,
30+
} from 'react-native';
2331

2432
const animationTypes = ['slide', 'none', 'fade'] as const;
2533
const presentationStyles = [
@@ -37,6 +45,7 @@ const supportedOrientations = [
3745
] as const;
3846

3947
const backdropColors = ['red', 'blue', undefined];
48+
const detents = ['200', '500', 'large'];
4049

4150
function ModalPresentation() {
4251
const onDismiss = useCallback(() => {
@@ -72,6 +81,7 @@ function ModalPresentation() {
7281
visible: false,
7382
backdropColor: undefined,
7483
});
84+
const [useDetents, setUseDetents] = useState(false);
7585
const presentationStyle = props.presentationStyle;
7686
const hardwareAccelerated = props.hardwareAccelerated;
7787
const statusBarTranslucent = props.statusBarTranslucent;
@@ -149,6 +159,13 @@ function ModalPresentation() {
149159
}
150160
/>
151161
</View>
162+
<View style={styles.inlineBlock}>
163+
<RNTesterText style={styles.title}>Use Detents ⚫️</RNTesterText>
164+
<Switch
165+
value={useDetents}
166+
onValueChange={enabled => setUseDetents(enabled)}
167+
/>
168+
</View>
152169
<View style={styles.block}>
153170
<RNTesterText style={styles.title}>Presentation Style ⚫️</RNTesterText>
154171
<View style={styles.row}>
@@ -295,9 +312,12 @@ function ModalPresentation() {
295312
</RNTesterButton>
296313
<Modal
297314
{...props}
315+
detents={useDetents ? detents : undefined}
298316
onRequestClose={onRequestClose}
299317
onOrientationChange={onOrientationChange}>
300-
<View style={styles.modalContainer}>
318+
<ScrollView
319+
style={styles.modalContainer}
320+
contentContainerStyle={{justifyContent: 'center'}}>
301321
<View style={[styles.modalInnerContainer, {backgroundColor}]}>
302322
<Text testID="modal_animationType_text">
303323
This modal was presented with animationType: '
@@ -314,6 +334,9 @@ function ModalPresentation() {
314334
</RNTesterButton>
315335
{controls}
316336
</View>
337+
</ScrollView>
338+
<View style={styles.footer}>
339+
<Text>Footer position: absolute</Text>
317340
</View>
318341
</Modal>
319342
<View style={styles.block}>
@@ -367,7 +390,6 @@ const styles = StyleSheet.create({
367390
},
368391
modalContainer: {
369392
flex: 1,
370-
justifyContent: 'center',
371393
padding: 20,
372394
},
373395
modalInnerContainer: {
@@ -379,6 +401,17 @@ const styles = StyleSheet.create({
379401
fontSize: 12,
380402
color: 'red',
381403
},
404+
footer: {
405+
height: 60,
406+
width: '100%',
407+
position: 'absolute',
408+
bottom: 0,
409+
left: 0,
410+
right: 0,
411+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
412+
justifyContent: 'center',
413+
alignItems: 'center',
414+
},
382415
});
383416

384417
export default ({

0 commit comments

Comments
 (0)