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
|
-
|
|
5
|
+
**Truly native bottom sheets and popups for React Native, powered by [Nitro Modules](https://nitro.margelo.com/).**
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/react-native-nitro-modal)
|
|
8
|
+
[](https://www.npmjs.com/package/react-native-nitro-modal)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+

|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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":"
|
|
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.
|
|
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
|
-
|
|
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<
|