Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/src/main/java/com/multiplemodals/RNTModalView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ class RNTModalView(context: Context): ViewGroup(context), LifecycleEventListener
return modalView.childCount
}

override fun getChildAt(index: Int): View {
override fun getChildAt(index: Int): View? {
return modalView.getChildAt(index)
}

Expand Down
57 changes: 33 additions & 24 deletions example/demo-components/src/DemoScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import {ComponentType, useCallback, useMemo, useState} from 'react';
import {ScrollView, StyleSheet, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import {ScenarioCard} from './components/scenario-card/ScenarioCard';
import {Typography} from './components/typography/Typography';
import {AnimatedFadeModal} from './modals/animated-fade/AnimatedFadeModal';
import {BlockingModal} from './modals/blocking/BlockingModal';
import {BlurredModal} from './modals/blurred/BlurredModal';
import {DefaultModal} from './modals/default/DefaultModal';
import {FullScreenNoBackgroundModal} from './modals/full-screen-no-bg/FullScreenNoBackgroundModal';
import {GesturedModal} from './modals/gestured/GesturedModal';
import {InBottomTabsModal} from './modals/in-bottom-tabs-modal/InBottomTabsModal';
import {ReanimatedModal} from './modals/reanimated/ReanimatedModal';
import {SimpleModal} from './modals/simple/SimpleModal';
import {AnimatedSlideModal} from './modals/slide/AnimatedSlideModal';
import {WithNavigationInsideModal} from './modals/with-navigation-inside/WithNavigationInsideModal';
import {useTheme} from './theme/colors';
import {IS_FABRIC} from './constants';
import {EmbeddedModal} from './modals/embedded/EmbeddedModal';
import { ComponentType, useCallback, useMemo, useState } from 'react';
import { ScrollView, StyleSheet, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { ScenarioCard } from './components/scenario-card/ScenarioCard';
import { Typography } from './components/typography/Typography';
import { AnimatedFadeModal } from './modals/animated-fade/AnimatedFadeModal';
import { BlockingModal } from './modals/blocking/BlockingModal';
import { BlurredModal } from './modals/blurred/BlurredModal';
import { DefaultModal } from './modals/default/DefaultModal';
import { FullScreenNoBackgroundModal } from './modals/full-screen-no-bg/FullScreenNoBackgroundModal';
import { GesturedModal } from './modals/gestured/GesturedModal';
import { InBottomTabsModal } from './modals/in-bottom-tabs-modal/InBottomTabsModal';
import { ReanimatedModal } from './modals/reanimated/ReanimatedModal';
import { SimpleModal } from './modals/simple/SimpleModal';
import { AnimatedSlideModal } from './modals/slide/AnimatedSlideModal';
import { WithNavigationInsideModal } from './modals/with-navigation-inside/WithNavigationInsideModal';
import { useTheme } from './theme/colors';
import { IS_FABRIC } from './constants';
import { EmbeddedModal } from './modals/embedded/EmbeddedModal';
import { FlashModal } from './modals/flash-modal/FlashModal';

type DemoCase = {
id: string;
Expand All @@ -28,7 +29,7 @@ type DemoCase = {
};

export const DemoScreen = () => {
const {colors} = useTheme();
const { colors } = useTheme();

const [activeCases, setActiveCases] = useState<DemoCase[]>([]);

Expand Down Expand Up @@ -130,6 +131,12 @@ export const DemoScreen = () => {
description: 'A modal that contains a navigation stack inside it.',
Component: WithNavigationInsideModal,
},
{
id: 'flash-modal',
title: 'Flash Modal',
description: 'Tap Flash: a submodal is dismissed before it renders',
Component: FlashModal,
},
],
[openModal],
);
Expand Down Expand Up @@ -163,16 +170,18 @@ export const DemoScreen = () => {

return (
<SafeAreaView
style={[styles.safeArea, {backgroundColor: colors.background}]}>
style={[styles.safeArea, { backgroundColor: colors.background }]}
>
<ScrollView
style={styles.scroll}
contentContainerStyle={styles.scrollContent}>
<Typography testID="screen-top" style={styles.architecture}>
contentContainerStyle={styles.scrollContent}
>
<Typography testID='screen-top' style={styles.architecture}>
Current Architecture: {IS_FABRIC ? 'Fabric 🚀' : 'Paper ✈️'}
</Typography>

<View style={styles.casesContainer}>
{demoCases.map(({title, description, id}) => (
{demoCases.map(({ title, description, id }) => (
<ScenarioCard
key={id}
testID={`${id}-open-button`}
Expand Down
8 changes: 8 additions & 0 deletions example/demo-components/src/modals.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@
"e2e": {
"scenarios": ["simple_dismiss_by_button"]
}
},
{
"id": "flash-modal",
"title": "Flash Modal",
"description": "A modal whose submodal is dismissed before it renders.",
"e2e": {
"scenarios": ["flash_dismiss"]
}
}
]
}
74 changes: 74 additions & 0 deletions example/demo-components/src/modals/flash-modal/FlashModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { FC, useState } from 'react';
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
import { ModalView } from 'react-native-multiple-modals';
import { Button } from '../../components/button/Button';
import { useTheme } from '../../theme/colors';
import { BaseModalProps } from '../BaseModal';

export const FlashModal: FC<BaseModalProps> = ({
onRequestDismiss,
testID,
title,
}) => {
const { colors } = useTheme();
const [isLoading, setLoading] = useState(false);

const flash = () => {
setLoading(true);
setTimeout(() => setLoading(false), 0);
};

return (
<ModalView
onRequestDismiss={onRequestDismiss}
contentContainerStyle={styles.contentContainer}
>
<View
testID={`${testID}-modal`}
style={[
styles.modal,
{
backgroundColor: colors.background,
borderColor: colors.cardOutline,
},
]}
>
<Text style={[styles.title, { color: colors.textPrimary }]}>
{title}
</Text>
<View style={styles.buttons}>
<Button testID={`${testID}-flash-button`} onPress={flash}>
Flash
</Button>
<Button testID={`${testID}-close-button`} onPress={onRequestDismiss}>
Close
</Button>
</View>
</View>

{isLoading ? (
<ModalView contentContainerStyle={styles.contentContainer}>
<ActivityIndicator size='large' color={colors.textPrimary} />
</ModalView>
) : null}
</ModalView>
);
};

const styles = StyleSheet.create({
contentContainer: { alignItems: 'center', justifyContent: 'center' },
modal: {
width: '80%',
borderRadius: 24,
borderWidth: 1,
padding: 20,
gap: 16,
},
title: {
textAlign: 'center',
fontSize: 26,
lineHeight: 30,
fontWeight: '500',
},
buttons: { gap: 16 },
});
30 changes: 30 additions & 0 deletions example/e2e/flows/flash_dismiss.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
appId: ${APP_ID}
env:
TEST_NAME: flash_dismiss
---
# Regression guard for the "dismiss before first render" crash. Flashing a
# submodal (open, then dismiss on the next tick before it renders a frame)
- scrollUntilVisible:
direction: up
element:
id: screen-top
- scrollUntilVisible:
element:
id: ${MODAL_ID}-open-button
centerElement: true
- tapOn:
id: ${MODAL_ID}-open-button
- assertVisible:
id: ${MODAL_ID}-flash-button
- repeat:
times: 10
commands:
- tapOn:
id: ${MODAL_ID}-flash-button
# If any flash crashed the app, the button is gone and this assertion fails.
- assertVisible:
id: ${MODAL_ID}-flash-button
- tapOn:
id: ${MODAL_ID}-close-button
- assertNotVisible:
id: ${MODAL_ID}-close-button
24 changes: 18 additions & 6 deletions ios/Library/RNTModalViewController/RNTModalViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ - (instancetype)initWithDelegate:(id<RNTModalViewControllerDelegate>)delegate {
}

- (void)setupReactSubview:(UIView *)subview {
if (!self.reactSubviewContainer || !self.view) {
return;
}

[self.view addSubview:self.reactSubviewContainer];
self.reactSubviewContainer.translatesAutoresizingMaskIntoConstraints = NO;

Expand Down Expand Up @@ -65,16 +69,24 @@ - (void)presentOn:(UIViewController *)parentVC onView:(UIView *)parentView {

- (void)dismiss {
UIView *prevReactSubviewContainer = self.reactSubviewContainer;
self.reactSubviewContainer = [self.reactSubviewContainer snapshotViewAfterScreenUpdates:NO];
[prevReactSubviewContainer removeFromSuperview];
UIView *snapshot = [self.reactSubviewContainer snapshotViewAfterScreenUpdates:NO];

[self setupReactSubview:self.reactSubviewContainer];
[self.outAnimation prepareAnimation:self.reactSubviewContainer];
[self.outAnimation animate:self.reactSubviewContainer completion:^(BOOL finished) {
if (snapshot) {
self.reactSubviewContainer = snapshot;
[prevReactSubviewContainer removeFromSuperview];

[self setupReactSubview:self.reactSubviewContainer];
[self.outAnimation prepareAnimation:self.reactSubviewContainer];
[self.outAnimation animate:self.reactSubviewContainer completion:^(BOOL finished) {
[self willMoveToParentViewController:nil];
[self.view removeFromSuperview];
[self removeFromParentViewController];
}];
} else {
[self willMoveToParentViewController:nil];
[self.view removeFromSuperview];
[self removeFromParentViewController];
}];
}
}

- (void)addReactSubview:(UIView *)view {
Expand Down
Loading