react-native-nitro-modal 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -1,39 +1,278 @@
1
+ <div align="center">
2
+
1
3
  # react-native-nitro-modal
2
4
 
3
- react native native modal
5
+ **Truly native bottom sheets and popups for React Native, powered by [Nitro Modules](https://nitro.margelo.com/).**
4
6
 
5
- ## Installation
7
+ [![npm version](https://img.shields.io/npm/v/react-native-nitro-modal.svg?style=flat-square)](https://www.npmjs.com/package/react-native-nitro-modal)
8
+ [![npm downloads](https://img.shields.io/npm/dm/react-native-nitro-modal.svg?style=flat-square)](https://www.npmjs.com/package/react-native-nitro-modal)
9
+ [![license](https://img.shields.io/npm/l/react-native-nitro-modal.svg?style=flat-square)](LICENSE)
10
+ ![platforms](https://img.shields.io/badge/platforms-iOS%20%7C%20Android-lightgrey.svg?style=flat-square)
11
+
12
+ </div>
13
+
14
+ ---
15
+
16
+ `react-native-nitro-modal` renders your React content inside the platform's own modal primitives — `UISheetPresentationController` on iOS and Material `BottomSheetBehavior` on Android — so gestures, detent snapping, keyboard handling and transitions are handled natively, not re-implemented in JavaScript.
17
+
18
+ ## Features
19
+
20
+ - 📄 **Native bottom sheets** with multiple detents (`small`, `medium`, `large`, `fitContent`) and swipe-to-dismiss
21
+ - 🪟 **Centered popups** with native `scale` / `fade` transitions
22
+ - 📏 **Content-sized sheets** — `fitContent` measures your React content and grows with it
23
+ - ⌨️ **Keyboard aware** — the sheet or card follows the keyboard animation (`pan` or `resize`)
24
+ - 🌫️ **Customizable backdrop** — color, opacity and blur
25
+ - 🎛️ **Controlled or imperative** — drive it with an `isOpen` prop or through a `ref`
26
+ - 🔔 **Rich events** — present, dismiss (with reason), detent change, backdrop press, back button
27
+ - ⚡ **Built on Nitro** — JSI-backed native views with no bridge overhead
28
+ - 🟦 **Fully typed** TypeScript API
29
+
30
+ ## Requirements
6
31
 
32
+ | Dependency | Version |
33
+ | ------------------------------ | -------------------------------------------- |
34
+ | React Native | New Architecture enabled |
35
+ | `react-native-nitro-modules` | `^0.37.1` |
36
+ | iOS | 15.0+ (16.0+ for `small` and `fitContent`) |
37
+ | Android | API 24 (Android 7.0)+ |
38
+
39
+ > [!NOTE]
40
+ > Web and other platforms are not supported. On those platforms the component renders nothing and logs a one-time warning.
41
+
42
+ ## Installation
7
43
 
8
44
  ```sh
45
+ # npm
9
46
  npm install react-native-nitro-modal react-native-nitro-modules
10
47
 
11
- > `react-native-nitro-modules` is required as this library relies on [Nitro Modules](https://nitro.margelo.com/).
48
+ # yarn
49
+ yarn add react-native-nitro-modal react-native-nitro-modules
50
+ ```
51
+
52
+ Then install the iOS pods:
53
+
54
+ ```sh
55
+ cd ios && pod install
12
56
  ```
13
57
 
58
+ **Expo:** the library contains native code, so it works in a [development build](https://docs.expo.dev/develop/development-builds/introduction/) (`npx expo prebuild`) but not in Expo Go.
59
+
60
+ ## Quick start
61
+
62
+ ```tsx
63
+ import { useState } from 'react';
64
+ import { Button, Text, View } from 'react-native';
65
+ import { NitroModal } from 'react-native-nitro-modal';
66
+
67
+ export function Example() {
68
+ const [open, setOpen] = useState(false);
69
+
70
+ return (
71
+ <>
72
+ <Button title="Open sheet" onPress={() => setOpen(true)} />
73
+
74
+ <NitroModal
75
+ isOpen={open}
76
+ detents={['fitContent']}
77
+ showGrabber
78
+ onDismiss={() => setOpen(false)}
79
+ >
80
+ <View style={{ padding: 24 }}>
81
+ <Text>Hello from a native bottom sheet 👋</Text>
82
+ </View>
83
+ </NitroModal>
84
+ </>
85
+ );
86
+ }
87
+ ```
88
+
89
+ > [!IMPORTANT]
90
+ > In controlled mode, the user can close the modal natively (swipe, backdrop tap, back button). Always set your state back to `false` in `onDismiss`, otherwise `isOpen` will be out of sync.
14
91
 
15
92
  ## Usage
16
93
 
94
+ ### Bottom sheet with detents
95
+
96
+ Detents are listed smallest first. `initialDetentIndex` selects where the sheet opens.
97
+
98
+ ```tsx
99
+ <NitroModal
100
+ isOpen={open}
101
+ detents={['medium', 'large']}
102
+ initialDetentIndex={0}
103
+ showGrabber
104
+ backdropOpacity={0.25}
105
+ backdropBlur={12}
106
+ onDetentChange={(index) => console.log('detent', index)}
107
+ onDismiss={() => setOpen(false)}
108
+ >
109
+ <FlatList data={rows} renderItem={renderRow} nestedScrollEnabled />
110
+ </NitroModal>
111
+ ```
112
+
113
+ ### Popup
114
+
115
+ ```tsx
116
+ <NitroModal
117
+ isOpen={confirmOpen}
118
+ mode="popup"
119
+ popupAnimation="scale"
120
+ dismissOnBackdropPress={false}
121
+ onDismiss={() => setConfirmOpen(false)}
122
+ >
123
+ <View style={{ width: 300, padding: 24 }}>
124
+ <Text>Delete item?</Text>
125
+ <Button title="Cancel" onPress={() => setConfirmOpen(false)} />
126
+ </View>
127
+ </NitroModal>
128
+ ```
129
+
130
+ ### Imperative (uncontrolled) usage
131
+
132
+ Omit `isOpen` and control the modal through a ref.
133
+
134
+ ```tsx
135
+ import { useRef } from 'react';
136
+ import { NitroModal, type NitroModalRef } from 'react-native-nitro-modal';
137
+
138
+ const sheet = useRef<NitroModalRef>(null);
17
139
 
18
- ```js
19
- import { NitroModalView } from "react-native-nitro-modal";
140
+ <Button title="Open" onPress={() => sheet.current?.present()} />
20
141
 
21
- // ...
142
+ <NitroModal ref={sheet} detents={['small', 'medium']}>
143
+ <Button title="Expand" onPress={() => sheet.current?.snapToDetent(1)} />
144
+ <Button title="Close" onPress={() => sheet.current?.dismiss()} />
145
+ </NitroModal>
146
+ ```
147
+
148
+ ### Keyboard handling
149
+
150
+ ```tsx
151
+ <NitroModal isOpen={open} keyboardBehavior="resize" onDismiss={close}>
152
+ <TextInput placeholder="Type something…" />
153
+ </NitroModal>
154
+ ```
155
+
156
+ | Value | Behavior |
157
+ | ---------- | ------------------------------------------------------------------------------------------------ |
158
+ | `'pan'` | The sheet/card moves above the keyboard in sync with its animation. Content keeps its size. |
159
+ | `'resize'` | Like `pan`, and the area given to the content shrinks so scrollable content fits above the keyboard. |
160
+ | `'none'` | The keyboard is ignored. |
161
+
162
+ ## API
163
+
164
+ ### `<NitroModal />` props
165
+
166
+ | Prop | Type | Default | Description |
167
+ | ------------------------ | ---------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------- |
168
+ | `isOpen` | `boolean` | — | Controls visibility. Leave undefined to use the [ref API](#nitromodalref) instead. |
169
+ | `mode` | `'bottomSheet' \| 'popup'` | `'bottomSheet'` | How the modal is presented. |
170
+ | `detents` | `SheetDetent[]` | `['fitContent']` | Sheet heights, smallest first. Android uses at most three. |
171
+ | `initialDetentIndex` | `number` | `0` | Index into `detents` the sheet opens at. |
172
+ | `backdropColor` | `ColorValue` | `'black'` | Backdrop color. |
173
+ | `backdropOpacity` | `number` | `0.4` | Backdrop opacity, `0`–`1`. |
174
+ | `backdropBlur` | `number` | `0` | Blur radius (dp/pt) behind the modal. `0` disables it. See [platform notes](#platform-notes). |
175
+ | `dismissOnBackdropPress` | `boolean` | `true` | Tapping the backdrop closes the modal. |
176
+ | `dismissOnSwipe` | `boolean` | `true` | Swiping down closes a bottom sheet. |
177
+ | `dismissOnBackButton` | `boolean` | `true` | The Android back button/gesture closes the modal. |
178
+ | `showGrabber` | `boolean` | `false` | Shows the drag handle on a bottom sheet. |
179
+ | `cornerRadius` | `number` | platform default | Corner radius of the sheet/card. |
180
+ | `backgroundColor` | `ColorValue` | system surface | Background of the sheet/card. |
181
+ | `keyboardBehavior` | `'pan' \| 'resize' \| 'none'` | `'pan'` | How the modal reacts to the software keyboard. |
182
+ | `popupAnimation` | `'scale' \| 'fade' \| 'none'` | `'scale'` | Enter/exit transition of a popup. |
183
+ | `contentContainerStyle` | `StyleProp<ViewStyle>` | — | Style of the view that wraps `children` inside the modal. |
184
+ | `testID` | `string` | — | Applied to the content container. |
185
+ | `ref` | `Ref<NitroModalRef>` | — | Imperative handle. |
186
+
187
+ ### Events
188
+
189
+ | Prop | Signature | Description |
190
+ | ------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------- |
191
+ | `onPresent` | `() => void` | The present transition finished. |
192
+ | `onDismiss` | `(reason: DismissReason) => void` | The modal is fully gone. Fires exactly once per presentation. |
193
+ | `onDetentChange` | `(index: number) => void` | A bottom sheet settled on a different detent. |
194
+ | `onBackdropPress` | `() => void` | The backdrop was tapped (fires even when `dismissOnBackdropPress` is `false`). |
195
+ | `onBackButtonPress` | `() => void` | The Android hardware/gesture back was pressed. |
196
+
197
+ ### `NitroModalRef`
198
+
199
+ | Method | Description |
200
+ | ----------------------------- | ----------------------------------------------------------------------------------- |
201
+ | `present()` | Opens the modal. Uncontrolled usage only; ignored with a warning when `isOpen` is set. |
202
+ | `dismiss()` | Closes the modal. `onDismiss` then fires with `'programmatic'`. |
203
+ | `snapToDetent(index: number)` | Animates a bottom sheet to `detents[index]`. |
204
+
205
+ ### Types
206
+
207
+ ```ts
208
+ type ModalMode = 'bottomSheet' | 'popup';
209
+
210
+ type SheetDetent =
211
+ | 'small' // ~25% of the available height
212
+ | 'medium' // ~50% of the available height
213
+ | 'large' // the full available height
214
+ | 'fitContent'; // the measured height of your content
215
+
216
+ type KeyboardBehavior = 'pan' | 'resize' | 'none';
217
+
218
+ type PopupAnimation = 'scale' | 'fade' | 'none';
22
219
 
23
- <NitroModalView color="tomato" />
220
+ type DismissReason = 'programmatic' | 'swipe' | 'backdrop' | 'backButton';
24
221
  ```
25
222
 
223
+ All types are exported from the package root:
224
+
225
+ ```ts
226
+ import type {
227
+ DismissReason,
228
+ KeyboardBehavior,
229
+ ModalMode,
230
+ NitroModalProps,
231
+ NitroModalRef,
232
+ PopupAnimation,
233
+ SheetDetent,
234
+ } from 'react-native-nitro-modal';
235
+ ```
236
+
237
+ ## How it works
238
+
239
+ `<NitroModal>` mounts a zero-size placeholder in your React tree. When opened, the native side presents a real view controller (iOS) or window (Android) and hosts your React children inside it. Children mount when the modal opens and stay mounted until the native exit animation completes, so content never disappears mid-transition. The native side also reports the exact area available to the content (accounting for rotation, keyboard and sheet size), and the content container is sized accordingly.
240
+
241
+ ## Platform notes
242
+
243
+ **iOS**
244
+
245
+ - Bottom sheets use `UISheetPresentationController`.
246
+ - On iOS 15, only the system `medium` and `large` detents exist; `small` and `fitContent` fall back to `medium`.
247
+ - UIKit has no public blur-radius API, so `backdropBlur` is approximated by blending a thin system material.
248
+
249
+ **Android**
250
+
251
+ - Bottom sheets use Material Components' `BottomSheetBehavior`, which supports at most three detents.
252
+ - `backdropBlur` requires Android 12 (API 31)+; it is ignored on older versions.
253
+ - `onBackButtonPress` and `dismissOnBackButton` apply to both the hardware back button and the system back gesture.
254
+
255
+ **Colors**
256
+
257
+ - `backdropColor` and `backgroundColor` accept any color string or number supported by `processColor`. `PlatformColor` and `DynamicColorIOS` values are not supported yet.
258
+
259
+ ## Example app
260
+
261
+ The repository includes an example app that covers content-sized sheets, multi-detent sheets with lists, popups and ref-driven usage.
262
+
263
+ ```sh
264
+ yarn
265
+ yarn example ios # or: yarn example android
266
+ ```
26
267
 
27
268
  ## Contributing
28
269
 
29
- - [Development workflow](CONTRIBUTING.md#development-workflow)
30
- - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
31
- - [Code of conduct](CODE_OF_CONDUCT.md)
270
+ Contributions are welcome! See the [contributing guide](CONTRIBUTING.md) to learn how to set up the development workflow and send a pull request, and please follow the [code of conduct](CODE_OF_CONDUCT.md).
32
271
 
33
272
  ## License
34
273
 
35
- MIT
274
+ [MIT](LICENSE) © [Mohammad Navabi](https://github.com/mohamadnavabi)
36
275
 
37
276
  ---
38
277
 
39
- Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
278
+ Built with [Nitro Modules](https://nitro.margelo.com/) and [create-react-native-library](https://github.com/callstack/react-native-builder-bob).
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  import { getHostComponent } from 'react-native-nitro-modules';
4
- const NitroModalConfig = require('../nitrogen/generated/shared/json/NitroModalConfig.json');
4
+ // Resolved through the package's own `exports` so the path works from both
5
+ // `src/` and the compiled `lib/module/` output (a relative `../nitrogen` breaks there).
6
+ const NitroModalConfig = require('react-native-nitro-modal/nitrogen/generated/shared/json/NitroModalConfig.json');
5
7
  export const NitroModalView = getHostComponent('NitroModal', () => NitroModalConfig);
6
8
  //# sourceMappingURL=NitroModalView.native.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["getHostComponent","NitroModalConfig","require","NitroModalView"],"sourceRoot":"../../src","sources":["NitroModalView.native.tsx"],"mappings":";;AAAA,SAASA,gBAAgB,QAAQ,4BAA4B;AAC7D,MAAMC,gBAAgB,GAAGC,OAAO,CAAC,yDAAyD,CAAC;AAG3F,OAAO,MAAMC,cAAc,GAAGH,gBAAgB,CAG5C,YAAY,EAAE,MAAMC,gBAAgB,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["getHostComponent","NitroModalConfig","require","NitroModalView"],"sourceRoot":"../../src","sources":["NitroModalView.native.tsx"],"mappings":";;AAAA,SAASA,gBAAgB,QAAQ,4BAA4B;AAC7D;AACA;AACA,MAAMC,gBAAgB,GAAGC,OAAO,CAAC,+EAA+E,CAAC;AAGjH,OAAO,MAAMC,cAAc,GAAGH,gBAAgB,CAG5C,YAAY,EAAE,MAAMC,gBAAgB,CAAC","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"NitroModalView.native.d.ts","sourceRoot":"","sources":["../../../src/NitroModalView.native.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,uBAAoB,CAAC;AAE7E,eAAO,MAAM,cAAc,0FAGY,CAAC"}
1
+ {"version":3,"file":"NitroModalView.native.d.ts","sourceRoot":"","sources":["../../../src/NitroModalView.native.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,uBAAoB,CAAC;AAE7E,eAAO,MAAM,cAAc,0FAGY,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-modal",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "react native native modal",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -10,6 +10,7 @@
10
10
  "types": "./lib/typescript/src/index.d.ts",
11
11
  "default": "./lib/module/index.js"
12
12
  },
13
+ "./nitrogen/generated/shared/json/*": "./nitrogen/generated/shared/json/*",
13
14
  "./package.json": "./package.json"
14
15
  },
15
16
  "files": [
@@ -1,5 +1,7 @@
1
1
  import { getHostComponent } from 'react-native-nitro-modules';
2
- const NitroModalConfig = require('../nitrogen/generated/shared/json/NitroModalConfig.json');
2
+ // Resolved through the package's own `exports` so the path works from both
3
+ // `src/` and the compiled `lib/module/` output (a relative `../nitrogen` breaks there).
4
+ const NitroModalConfig = require('react-native-nitro-modal/nitrogen/generated/shared/json/NitroModalConfig.json');
3
5
  import type { NitroModalMethods, NitroModalProps } from './NitroModal.nitro';
4
6
 
5
7
  export const NitroModalView = getHostComponent<