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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const YourComponent = () => {
};
```

More Examples: https://github.com/paufau/react-native-multiple-modals-examples
More Examples: https://github.com/paufau/react-native-multiple-modals/example/demo-components

---

Expand Down
41 changes: 41 additions & 0 deletions example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local

# typescript
*.tsbuildinfo

# generated native folders
/ios
/android
5 changes: 5 additions & 0 deletions example/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { DemoApp } from './demo-components/src/DemoApp';

export default function App() {
return <DemoApp />;
}
29 changes: 29 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# example

Expo app that runs `react-native-multiple-modals` (from `../src`) against the
latest Expo SDK, on iOS, Android, and web. The demo gallery lives in
`demo-components/` (vendored from
[react-native-multiple-modals-examples](https://github.com/paufau/react-native-multiple-modals-examples)).

Because the library ships custom native (Fabric) code, iOS/Android run as a
**dev build** — not Expo Go.

## Run

```bash
cd example
npm install # links the library via file:.. and installs deps
npx expo prebuild --clean # generates ios/ + android/ (regenerable; gitignored)

npx expo run:ios # iOS simulator
npx expo run:android # Android emulator/device
npx expo start --web # web (react-native-web)
```

## How it's wired

- `metro.config.js` maps `react-native-multiple-modals` → `../src` (consumes TS
source directly, no build step) and forces the library's peer deps to resolve
from this app's `node_modules`, blocking the repo root's copies.
- `react-native-multiple-modals` is a `file:..` dependency so `expo prebuild`
autolinks the native module.
27 changes: 27 additions & 0 deletions example/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"expo": {
"name": "example",
"slug": "example",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.multiplemodals.example"
},
"android": {
"package": "com.multiplemodals.example",
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/android-icon-foreground.png",
"backgroundImage": "./assets/android-icon-background.png",
"monochromeImage": "./assets/android-icon-monochrome.png"
},
"predictiveBackGestureEnabled": false
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
Binary file added example/assets/android-icon-background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added example/assets/android-icon-foreground.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added example/assets/android-icon-monochrome.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added example/assets/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added example/assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added example/assets/splash-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions example/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
// Reanimated v4 uses the worklets plugin (must be listed last).
plugins: ['react-native-worklets/plugin'],
};
};
30 changes: 30 additions & 0 deletions example/demo-components/src/DemoApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {DemoScreen} from './DemoScreen';
import {LogBox, StatusBar, useColorScheme} from 'react-native';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
import {SafeAreaProvider} from 'react-native-safe-area-context';

LogBox.ignoreAllLogs();

export function DemoApp() {
const isDark = useColorScheme() === 'dark';

return (
<GestureHandlerRootView style={{flex: 1}}>
<SafeAreaProvider>
<StatusBar
translucent
{...(isDark
? {
backgroundColor: 'black',
barStyle: 'light-content',
}
: {
backgroundColor: 'white',
barStyle: 'dark-content',
})}
/>
<DemoScreen />
</SafeAreaProvider>
</GestureHandlerRootView>
);
}
219 changes: 219 additions & 0 deletions example/demo-components/src/DemoScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
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';

type DemoCase = {
id: string;
title: string;
description: string;
Component: ComponentType<any>;
additionalProps?: object;
isExclusive?: boolean;
};

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

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

const openModal = useCallback((caseId: string) => {
const demoCase = demoCases.find(c => c.id === caseId);

if (demoCase) {
setActiveCases(prev => [...prev, demoCase]);
} else {
console.error(`Demo case with id "${caseId}" not found.`);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const closeModal = useCallback((caseId: string) => {
setActiveCases(prev => prev.filter(c => c.id !== caseId));
}, []);

const demoCases = useMemo<DemoCase[]>(
() => [
{
id: 'default',
title: 'Default',
description: 'A default React Native modal',
Component: DefaultModal,
additionalProps: {
onOpenLibraryModal: () => openModal('blocking'),
},
},
{
id: 'simple',
title: 'Simple',
description: 'No animations. Can be dismissed by tapping outside.',
Component: SimpleModal,
},
{
id: 'embedded',
title: 'Embedded',
description:
'Has a modal inside it. Opening the inner modal should affect the layout.',
Component: EmbeddedModal,
},
{
id: 'animated-fade',
title: 'Animated Fade',
description: 'Animated fade appearance and disappearance.',
Component: AnimatedFadeModal,
},
{
id: 'animated-slide',
title: 'Animated Slide',
description: 'Animated slide appearance and disappearance.',
Component: AnimatedSlideModal,
},
{
id: 'blocking',
title: 'Blocking',
description:
'A modal that blocks interaction with the background. Appears from the bottom.',
Component: BlockingModal,
additionalProps: {
onOpenAnother: () => openModal('reanimated'),
},
},
{
id: 'blurred',
title: 'Blurred',
description: 'A modal with a custom blurred background.',
Component: BlurredModal,
},
{
id: 'gestured',
title: 'Gestured',
description: 'A modal that supports gestures for dragging.',
Component: GesturedModal,
},
{
id: 'reanimated',
title: 'Reanimated',
description: 'A modal using Reanimated for advanced animations.',
Component: ReanimatedModal,
},
{
id: 'in-bottom-tab',
title: 'Modal Above Bottom Tabs',
description: 'A modal displayed above a bottom tab navigator.',
Component: InBottomTabsModal,
isExclusive: true,
},
{
id: 'full-screen',
title: 'Full Screen',
description: 'A full-screen modal.',
Component: FullScreenNoBackgroundModal,
},
{
id: 'with-navigation-inside',
title: 'With Navigation Inside Modal',
description: 'A modal that contains a navigation stack inside it.',
Component: WithNavigationInsideModal,
},
],
[openModal],
);

const isLastCaseExclusive = useMemo(() => {
const lastCase = activeCases[activeCases.length - 1];
if (!lastCase) {
return false;
}
return lastCase.isExclusive;
}, [activeCases]);

const renderModal = useCallback(
(item: DemoCase) => (
<item.Component
key={item.id}
title={item.title}
testID={item.id}
onRequestDismiss={() => closeModal(item.id)}
{...item.additionalProps}
/>
),
[closeModal],
);

if (isLastCaseExclusive && activeCases.length > 0) {
const lastCase = activeCases[activeCases.length - 1];

return renderModal(lastCase);
}

return (
<SafeAreaView
style={[styles.safeArea, {backgroundColor: colors.background}]}>
<ScrollView
style={styles.scroll}
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}) => (
<ScenarioCard
key={id}
testID={`${id}-open-button`}
title={title}
description={description}
onPress={() => {
openModal(id);
}}
/>
))}
</View>
</ScrollView>

{activeCases.map(renderModal)}
</SafeAreaView>
);
};

const styles = StyleSheet.create({
safeArea: {
flex: 1,
},
scroll: {
flex: 1,
},
architecture: {
marginTop: 32,
fontSize: 20,
fontWeight: '500',
},
scrollContent: {
paddingHorizontal: 24,
alignItems: 'center',
gap: 24,
},
casesContainer: {
flexWrap: 'wrap',
flexDirection: 'row',
justifyContent: 'center',
gap: 12,
maxWidth: 800,
marginBottom: 48,
},
});
Loading
Loading