react-native-unified-action-sheet 0.2.0 → 0.4.0
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/CHANGELOG.md +87 -1
- package/README.md +44 -112
- package/android/build.gradle +28 -1
- package/android/src/main/java/com/unifiedactionsheet/ActionSheetOptions.kt +41 -2
- package/android/src/main/java/com/unifiedactionsheet/PromptOptions.kt +27 -5
- package/android/src/main/java/com/unifiedactionsheet/SheetContentBuilder.kt +75 -9
- package/android/src/main/java/com/unifiedactionsheet/SheetPresenters.kt +61 -10
- package/android/src/main/java/com/unifiedactionsheet/UnifiedActionSheetModule.kt +14 -7
- package/android/src/material/java/com/unifiedactionsheet/BottomSheetPresenterFactory.kt +10 -0
- package/android/src/material/java/com/unifiedactionsheet/MaterialBottomSheetPresenter.kt +153 -0
- package/android/src/material/res/values/unified_action_sheet_bottom_sheet.xml +22 -0
- package/android/src/noMaterial/java/com/unifiedactionsheet/BottomSheetPresenterFactory.kt +10 -0
- package/app.plugin.js +1 -0
- package/ios/BottomSheetViewController.swift +210 -0
- package/ios/UnifiedActionSheet.mm +51 -5
- package/ios/UnifiedActionSheetImpl.swift +259 -22
- package/jest/index.d.ts +12 -9
- package/jest/index.js +37 -18
- package/lib/commonjs/NativeUnifiedActionSheet.js.map +1 -1
- package/lib/commonjs/index.js +102 -18
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/NativeUnifiedActionSheet.js.map +1 -1
- package/lib/module/index.js +102 -18
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts +10 -1
- package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts +5 -2
- package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/common-options.interface.d.ts +9 -0
- package/lib/typescript/commonjs/src/common-options.interface.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +6 -6
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/prompt-options.interface.d.ts +10 -6
- package/lib/typescript/commonjs/src/prompt-options.interface.d.ts.map +1 -1
- package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts +10 -1
- package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts.map +1 -1
- package/lib/typescript/module/src/action-sheet-options.interface.d.ts +5 -2
- package/lib/typescript/module/src/action-sheet-options.interface.d.ts.map +1 -1
- package/lib/typescript/module/src/common-options.interface.d.ts +9 -0
- package/lib/typescript/module/src/common-options.interface.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +6 -6
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/prompt-options.interface.d.ts +10 -6
- package/lib/typescript/module/src/prompt-options.interface.d.ts.map +1 -1
- package/package.json +12 -2
- package/plugin/withUnifiedActionSheet.js +58 -0
- package/src/NativeUnifiedActionSheet.ts +12 -2
- package/src/action-sheet-options.interface.ts +12 -1
- package/src/common-options.interface.ts +18 -0
- package/src/index.tsx +147 -42
- package/src/prompt-options.interface.ts +21 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,90 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0]
|
|
11
|
+
|
|
12
|
+
### Changed (breaking)
|
|
13
|
+
|
|
14
|
+
- **Results carry a reason.** `showActionSheetWithOptions()` resolves
|
|
15
|
+
`{ reason, buttonIndex }` instead of a bare index, and
|
|
16
|
+
`showPromptWithOptions()` resolves `{ reason, buttonIndex, text, password? }`
|
|
17
|
+
instead of `{ buttonIndex, text }` or `undefined`. `reason` is `'selected'`,
|
|
18
|
+
`'cancelled'` (the cancel button, a backdrop tap, back or a swipe down) or
|
|
19
|
+
`'dismissed'` (closed from code or by a JS reload). A dismissal used to
|
|
20
|
+
resolve `undefined`; it now resolves `{ reason: 'dismissed', buttonIndex:
|
|
21
|
+
undefined }`, and a dismissed prompt keeps the text that was in its field.
|
|
22
|
+
The result types are discriminated unions on `reason`.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// before
|
|
26
|
+
const index = await showActionSheetWithOptions(options);
|
|
27
|
+
if (index === undefined) { /* dismissed */ }
|
|
28
|
+
|
|
29
|
+
// after
|
|
30
|
+
const { reason, buttonIndex } = await showActionSheetWithOptions(options);
|
|
31
|
+
if (reason === 'dismissed') { /* dismissed */ }
|
|
32
|
+
```
|
|
33
|
+
- **Prompt `onPress` receives an object.** It is called with
|
|
34
|
+
`{ text, password? }` instead of the text string:
|
|
35
|
+
`onPress: ({ text }) => rename(text)`.
|
|
36
|
+
- The Jest mock resolves the same shapes, deriving `reason` as the real module
|
|
37
|
+
does; `setNextPromptResult` accepts an optional `password`.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- `preferred` on a button: the default action, emphasized. On iOS it is the
|
|
42
|
+
alert's `preferredAction`, drawn in UIKit's own style (a filled button from
|
|
43
|
+
iOS 26, bold text before), so the return key presses it in a prompt; in the
|
|
44
|
+
bottom sheet it is a semibold row; UIKit ignores it in the standard action
|
|
45
|
+
sheet. On Android the row is bold, and in a prompt the keyboard's done key
|
|
46
|
+
presses it.
|
|
47
|
+
- `detents` for `presentationStyle: 'bottom'`: `'auto'`, `'medium'` and
|
|
48
|
+
`'large'`. The sheet opens at the first and can be dragged to the others.
|
|
49
|
+
- Prompt `type`: `'plain-text'`, `'secure-text'` or `'login-password'`, as in
|
|
50
|
+
React Native's `Alert.prompt`. `'login-password'` adds a masked password
|
|
51
|
+
field (`passwordPlaceholder`), resolves `password`, and marks both fields
|
|
52
|
+
for iOS AutoFill and Android autofill. `secureTextEntry` still works, as
|
|
53
|
+
`type: 'secure-text'`.
|
|
54
|
+
- `requiresText` on a prompt button keeps it disabled while a field is empty.
|
|
55
|
+
- New exported types: `ActionSheetResultInterface`, `ActionSheetDetent`,
|
|
56
|
+
`CloseReason`, `CloseResultInterface` and `PromptValuesInterface`.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- iOS: alerts and prompts were never freed after closing. Each action's handler
|
|
61
|
+
held its presentation, which held the alert, which held the action.
|
|
62
|
+
|
|
63
|
+
## [0.3.0]
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- `presentationStyle: 'bottom'`: a native bottom sheet on both platforms, with
|
|
68
|
+
a drag handle and swipe to dismiss. A long list opens part-way and drags up
|
|
69
|
+
to expand. On iOS it is a `UISheetPresentationController` sheet with native
|
|
70
|
+
rows (on iPad, the action sheet's popover instead); on Android, a Material 3
|
|
71
|
+
bottom sheet. `anchor` is ignored for it.
|
|
72
|
+
- Material is opt-in, so apps that don't use the bottom sheet stay free of the
|
|
73
|
+
dependency. Set `unifiedActionSheet.material=true` in
|
|
74
|
+
`android/gradle.properties`. Only then does the library add
|
|
75
|
+
`com.google.android.material` (1.12.0 by default; override it with
|
|
76
|
+
`ext.materialVersion` or `unifiedActionSheet.materialVersion`).
|
|
77
|
+
- An Expo config plugin that writes those properties:
|
|
78
|
+
`plugins: [['react-native-unified-action-sheet', { material: true }]]`, with
|
|
79
|
+
an optional `materialVersion`. `@expo/config-plugins` is an optional peer
|
|
80
|
+
dependency.
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
|
|
84
|
+
- On iOS, a sheet, alert or prompt stayed on screen after a JS reload (pressing
|
|
85
|
+
`r` in Metro). It now closes on reload, as it already did on Android, and its
|
|
86
|
+
promise is left unresolved, since it belonged to the discarded runtime.
|
|
87
|
+
|
|
88
|
+
### Notes
|
|
89
|
+
|
|
90
|
+
- Without the Material opt-in, `'bottom'` on Android falls back to the centered
|
|
91
|
+
dialog, and warns once in development.
|
|
92
|
+
- Prompts do not support `'bottom'`; they stay centered on both platforms.
|
|
93
|
+
|
|
10
94
|
## [0.2.0]
|
|
11
95
|
|
|
12
96
|
### Added
|
|
@@ -75,7 +159,9 @@ Initial release.
|
|
|
75
159
|
- No `com.google.android.material` dependency, and no runtime dependencies —
|
|
76
160
|
`react` and `react-native` are peers.
|
|
77
161
|
|
|
78
|
-
[unreleased]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.
|
|
162
|
+
[unreleased]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.4.0...HEAD
|
|
163
|
+
[0.4.0]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.3.0...v0.4.0
|
|
164
|
+
[0.3.0]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.2.0...v0.3.0
|
|
79
165
|
[0.2.0]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.1.1...v0.2.0
|
|
80
166
|
[0.1.1]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.1.0...v0.1.1
|
|
81
167
|
[0.1.0]: https://github.com/galikvalkin/react-native-unified-action-sheet/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
# react-native-unified-action-sheet
|
|
2
2
|
|
|
3
|
-
Unified action sheet
|
|
3
|
+
Unified native action sheet for React Native, one API on iOS and Android.
|
|
4
4
|
|
|
5
|
-
|
|
|
5
|
+
| | Light | Dark |
|
|
6
6
|
| :---: | :---: | :---: |
|
|
7
|
-
| <img src="https://raw.githubusercontent.com/galikvalkin/react-native-unified-action-sheet/HEAD/docs/android-demo.gif" width="280" alt="Android demo" /> | <img src="https://raw.githubusercontent.com/galikvalkin/react-native-unified-action-sheet/HEAD/docs/android-demo-dark-mode.gif" width="280" alt="Android
|
|
7
|
+
| **Android** | <img src="https://raw.githubusercontent.com/galikvalkin/react-native-unified-action-sheet/HEAD/docs/images/android-demo-light-mode.gif" width="280" alt="Android demo in light mode" /> | <img src="https://raw.githubusercontent.com/galikvalkin/react-native-unified-action-sheet/HEAD/docs/images/android-demo-dark-mode.gif" width="280" alt="Android demo in dark mode" /> |
|
|
8
|
+
| **iOS** | <img src="https://raw.githubusercontent.com/galikvalkin/react-native-unified-action-sheet/HEAD/docs/images/ios-demo-light-mode.gif" width="280" alt="iOS demo in light mode" /> | <img src="https://raw.githubusercontent.com/galikvalkin/react-native-unified-action-sheet/HEAD/docs/images/ios-demo-dark-mode.gif" width="280" alt="iOS demo in dark mode" /> |
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
</p>
|
|
14
|
-
|
|
15
|
-
- **Fully native, always on top.** The sheet gets its own platform window, so it renders above your whole view tree — including an already-open [`Modal`](https://reactnative.dev/docs/modal). JS-rendered sheets live *inside* the component tree, where `overflow`, `zIndex`, a transform or an open modal can clip or bury them.
|
|
16
|
-
- **One API, native on both platforms**, so the same options behave the same way.
|
|
17
|
-
- **Prompts included.** React Native's `Alert.prompt` is iOS-only and silently does nothing on Android; `showPromptWithOptions()` works on both.
|
|
18
|
-
- Works on React Native **0.81 (old and new architecture)** through the **latest** release.
|
|
19
|
-
- **No extra native dependencies** on either platform, and zero runtime dependencies (`react` / `react-native` peers only).
|
|
10
|
+
- **Fully native, always on top.** The sheet gets its own platform window, so it renders above your whole view tree, including an open [`Modal`](https://reactnative.dev/docs/modal). JS-rendered sheets live inside the component tree, where `overflow`, `zIndex` or a modal can clip or bury them.
|
|
11
|
+
- **One API on both platforms**: action sheets, anchored popovers, bottom sheets and text prompts.
|
|
12
|
+
- **Prompts on Android too.** React Native's `Alert.prompt` is iOS-only; `showPromptWithOptions()` works on both.
|
|
13
|
+
- **No dependencies.** `react` and `react-native` are peers; the Android bottom sheet's Material is opt-in.
|
|
20
14
|
|
|
21
15
|
## Installation
|
|
22
16
|
|
|
@@ -26,7 +20,9 @@ npm install react-native-unified-action-sheet
|
|
|
26
20
|
yarn add react-native-unified-action-sheet
|
|
27
21
|
```
|
|
28
22
|
|
|
29
|
-
Autolinking handles the rest
|
|
23
|
+
Autolinking handles the rest. In Expo, use a development build (`npx expo run:ios` / `npx expo run:android`); custom native modules do not work in Expo Go.
|
|
24
|
+
|
|
25
|
+
**Optional:** for the Android bottom sheet, set `unifiedActionSheet.material=true` in `android/gradle.properties`, or add the Expo plugin. See [Bottom sheets](docs/bottom-sheet.md).
|
|
30
26
|
|
|
31
27
|
## Usage
|
|
32
28
|
|
|
@@ -45,41 +41,19 @@ await showActionSheetWithOptions({
|
|
|
45
41
|
});
|
|
46
42
|
```
|
|
47
43
|
|
|
48
|
-
`
|
|
49
|
-
|
|
50
|
-
| | promise resolves with | `onPress` runs |
|
|
51
|
-
| --- | --- | --- |
|
|
52
|
-
| Tapping a button | its index | that button's |
|
|
53
|
-
| Backdrop tap, back button, swipe down | the `'cancel'` button's index, or `-1` if there is none | the cancel button's, if there is one |
|
|
54
|
-
| `dismissActionSheet()` | `undefined` | nothing |
|
|
55
|
-
|
|
56
|
-
The promise never rejects on its own — only if one of your own `onPress` handlers throws.
|
|
57
|
-
|
|
58
|
-
### Anchored sheets
|
|
59
|
-
|
|
60
|
-
`presentationStyle: 'anchored'` attaches the sheet to a view. Pass the ref itself; the library measures it and sends only the resulting rectangle across:
|
|
61
|
-
|
|
62
|
-
```tsx
|
|
63
|
-
const anchorRef = useRef<View>(null);
|
|
44
|
+
The call resolves `{ reason, buttonIndex }`, and the tapped button's `onPress` runs. `reason` is `'selected'`, `'cancelled'` (the cancel button, a backdrop tap, back or a swipe) or `'dismissed'` (closed from code). The promise never rejects. See [Behavior](docs/behavior.md).
|
|
64
45
|
|
|
65
|
-
|
|
66
|
-
ref={anchorRef}
|
|
67
|
-
onPress={() =>
|
|
68
|
-
showActionSheetWithOptions({
|
|
69
|
-
options: [...],
|
|
70
|
-
presentationStyle: 'anchored',
|
|
71
|
-
anchor: anchorRef,
|
|
72
|
-
})
|
|
73
|
-
}
|
|
74
|
-
/>;
|
|
75
|
-
```
|
|
46
|
+
### Presentation styles
|
|
76
47
|
|
|
77
|
-
|
|
48
|
+
| `presentationStyle` | iOS | Android |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| unset | Action sheet from the bottom | Centered dialog |
|
|
51
|
+
| `'centered'` | Centered alert | Centered dialog |
|
|
52
|
+
| `'anchored'` | Popover on iPad, pointing at `anchor` | Popup attached to `anchor` |
|
|
53
|
+
| `'bottom'` | Sheet with a grabber (popover on iPad) | Material bottom sheet ([opt-in](docs/bottom-sheet.md)) |
|
|
78
54
|
|
|
79
55
|
### Prompts
|
|
80
56
|
|
|
81
|
-
`showPromptWithOptions()` is the same dialog with a text field:
|
|
82
|
-
|
|
83
57
|
```ts
|
|
84
58
|
import { showPromptWithOptions } from 'react-native-unified-action-sheet';
|
|
85
59
|
|
|
@@ -88,85 +62,43 @@ const result = await showPromptWithOptions({
|
|
|
88
62
|
placeholder: 'New name',
|
|
89
63
|
defaultValue: 'Untitled',
|
|
90
64
|
options: [
|
|
91
|
-
{ label: 'Save', onPress: (text) => rename(text) },
|
|
65
|
+
{ label: 'Save', preferred: true, onPress: ({ text }) => rename(text) },
|
|
92
66
|
{ label: 'Cancel', style: 'cancel' },
|
|
93
67
|
],
|
|
94
68
|
});
|
|
95
|
-
// { buttonIndex, text }
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Buttons behave as they do for the sheet, except `onPress` receives the field's
|
|
99
|
-
text. That text is whatever was in the field when the prompt closed, including
|
|
100
|
-
on a dismissal, so a draft is recoverable. Extra options: `placeholder`,
|
|
101
|
-
`defaultValue`, `secureTextEntry` and `keyboardType` (`'default' |
|
|
102
|
-
'email-address' | 'numeric' | 'phone-pad' | 'url'`).
|
|
103
|
-
|
|
104
|
-
A prompt is always centered — UIKit has no text field in an action sheet, so
|
|
105
|
-
`presentationStyle` and `anchor` do not apply.
|
|
106
|
-
|
|
107
|
-
### Dismissing from code
|
|
108
|
-
|
|
109
|
-
`dismissActionSheet()` closes the most recently opened sheet, `dismissAllActionSheets()` closes every open one. Both are no-ops when nothing is open, and the dismissed sheets resolve with `undefined`.
|
|
110
|
-
|
|
111
|
-
```ts
|
|
112
|
-
import {
|
|
113
|
-
dismissActionSheet,
|
|
114
|
-
dismissAllActionSheets,
|
|
115
|
-
} from 'react-native-unified-action-sheet';
|
|
69
|
+
// { reason, buttonIndex, text }
|
|
116
70
|
```
|
|
117
71
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
| Option | Type | iOS | Android | Description |
|
|
121
|
-
| --- | --- | :---: | :---: | --- |
|
|
122
|
-
| `options` | `ActionSheetButtonInterface[]` | ✅ | ✅ | The buttons, in order: `{ label, style?, disabled?, onPress? }`. |
|
|
123
|
-
| `style` (per button) | `'cancel' \| 'destructive'?` | ✅ | ✅ | `'cancel'` renders a separated row and resolves on backdrop tap / back; `'destructive'` uses the destructive color. Only the first `'cancel'` counts. |
|
|
124
|
-
| `disabled` (per button) | `boolean?` | ✅ | ✅ | Renders the row dimmed and unresponsive to taps. |
|
|
125
|
-
| `onPress` (per button) | `() => void?` | ✅ | ✅ | Runs when this button resolves the sheet. |
|
|
126
|
-
| `title` | `string?` | ✅ | ✅ | Sheet title. |
|
|
127
|
-
| `message` | `string?` | ✅ | ✅ | Secondary text under the title. |
|
|
128
|
-
| `presentationStyle` | `'centered' \| 'anchored'` | ✅ | ✅ | `'centered'` is a centered dialog; `'anchored'` attaches to `anchor` (an iPad popover on iOS). **Defaults differ**: Android `'centered'`, iOS the standard action sheet. |
|
|
129
|
-
| `anchor` | `ActionSheetAnchorInterface?` | ✅ | ✅ | A ref, or anything with `measureInWindow`. Without a measurable anchor an `'anchored'` sheet falls back to a centered dialog. |
|
|
130
|
-
| `anchorAlignment` | `'start' \| 'center'` | — | ✅ | Alignment of an `'anchored'` popup relative to its anchor. `'start'` (default) aligns leading edges, flipping in RTL. |
|
|
131
|
-
| `userInterfaceStyle` | `'light' \| 'dark'` | ✅ | ✅ | Forces the appearance; defaults to following the system setting. |
|
|
132
|
-
| `tintColor` | `string?` | ✅ | ✅ | Text color of non-destructive buttons. |
|
|
133
|
-
| `cancelButtonTintColor` | `string?` | ✅ | ✅ | Text color of the cancel button; overrides `tintColor` for that row. |
|
|
134
|
-
| `destructiveColor` | `string?` | ✅ | ✅ | Overrides the destructive row color (Android's palette error color, iOS system red). |
|
|
135
|
-
| `buttonTextAlignment` | `'start' \| 'center'` | — | ✅ | Alignment of button labels. Defaults to `'start'`, which follows layout direction. |
|
|
136
|
-
|
|
137
|
-
Every options and button type is exported for typing your own wrappers —
|
|
138
|
-
`ActionSheetOptionsInterface`, `PromptOptionsInterface`, `PromptResultInterface`
|
|
139
|
-
and the interfaces they are composed from.
|
|
140
|
-
|
|
141
|
-
### Platform notes
|
|
142
|
-
|
|
143
|
-
- **Which gestures dismiss differs.** On iOS a sheet can only be tapped away if it has a `'cancel'` button, and a `'centered'` one never can — UIKit treats it as strictly modal. Android's centered dialog always cancels on a backdrop tap. Give a sheet a cancel button if you want that gesture everywhere.
|
|
144
|
-
- **On iPad, a popover hides the cancel row**, since tapping outside already cancels. The index you receive is unaffected.
|
|
145
|
-
- **Sheets stack.** Opening one over another puts it on top, and each resolves its own promise. Opening a sheet over a `Modal` does not dismiss the modal.
|
|
146
|
-
- **Light or dark follows the system setting** unless `userInterfaceStyle` forces one, chosen when the sheet opens. It uses its own palette, so it looks the same in any host app.
|
|
147
|
-
|
|
148
|
-
## Testing
|
|
149
|
-
|
|
150
|
-
The package ships a Jest mock, so a screen that opens a sheet can be tested without the native module:
|
|
72
|
+
`type: 'login-password'` adds a password field, and `requiresText: true` keeps a button disabled until the fields are filled:
|
|
151
73
|
|
|
152
74
|
```ts
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
75
|
+
await showPromptWithOptions({
|
|
76
|
+
title: 'Sign in',
|
|
77
|
+
type: 'login-password',
|
|
78
|
+
placeholder: 'Email',
|
|
79
|
+
passwordPlaceholder: 'Password',
|
|
80
|
+
options: [
|
|
81
|
+
{
|
|
82
|
+
label: 'Sign in',
|
|
83
|
+
preferred: true,
|
|
84
|
+
requiresText: true,
|
|
85
|
+
onPress: ({ text, password }) => signIn(text, password),
|
|
86
|
+
},
|
|
87
|
+
{ label: 'Cancel', style: 'cancel' },
|
|
88
|
+
],
|
|
89
|
+
});
|
|
156
90
|
```
|
|
157
91
|
|
|
158
|
-
|
|
92
|
+
### Dismissing from code
|
|
159
93
|
|
|
160
|
-
|
|
161
|
-
import { setNextButtonIndex } from 'react-native-unified-action-sheet/jest';
|
|
94
|
+
`dismissActionSheet()` closes the most recently opened sheet, and `dismissAllActionSheets()` closes every open one. They resolve with `reason: 'dismissed'`.
|
|
162
95
|
|
|
163
|
-
|
|
164
|
-
await openTheSheet();
|
|
165
|
-
```
|
|
96
|
+
## Documentation
|
|
166
97
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
98
|
+
- [API reference](docs/api.md): every option, per platform
|
|
99
|
+
- [Behavior](docs/behavior.md): what each gesture resolves with, and platform differences
|
|
100
|
+
- [Bottom sheets](docs/bottom-sheet.md): both platforms, and enabling Material on Android
|
|
101
|
+
- [Testing](docs/testing.md): the shipped Jest mock
|
|
170
102
|
|
|
171
103
|
## Compatibility
|
|
172
104
|
|
package/android/build.gradle
CHANGED
|
@@ -2,7 +2,8 @@ buildscript {
|
|
|
2
2
|
ext.UnifiedActionSheet = [
|
|
3
3
|
kotlinVersion: "2.0.21",
|
|
4
4
|
minSdkVersion: 24,
|
|
5
|
-
compileSdkVersion: 36
|
|
5
|
+
compileSdkVersion: 36,
|
|
6
|
+
materialVersion: "1.12.0"
|
|
6
7
|
]
|
|
7
8
|
|
|
8
9
|
ext.getExtOrDefault = { prop ->
|
|
@@ -31,6 +32,20 @@ apply plugin: "kotlin-android"
|
|
|
31
32
|
|
|
32
33
|
apply plugin: "com.facebook.react"
|
|
33
34
|
|
|
35
|
+
// Material is opt-in, so apps that don't use presentationStyle 'bottom' stay
|
|
36
|
+
// free of the dependency. The app sets unifiedActionSheet.material=true in its
|
|
37
|
+
// gradle.properties (the Expo config plugin writes it); only then is the
|
|
38
|
+
// Material presenter compiled and the dependency added. src/main never
|
|
39
|
+
// references a Material class: it goes through BottomSheetPresenterFactory,
|
|
40
|
+
// which both variant source sets define.
|
|
41
|
+
def materialEnabled = (findProperty("unifiedActionSheet.material") ?: "false").toString().toBoolean()
|
|
42
|
+
|
|
43
|
+
// ext.materialVersion on the root project wins, then the gradle property, then
|
|
44
|
+
// the version this library is tested against.
|
|
45
|
+
def materialVersion = rootProject.ext.has("materialVersion")
|
|
46
|
+
? rootProject.ext.get("materialVersion")
|
|
47
|
+
: (findProperty("unifiedActionSheet.materialVersion") ?: UnifiedActionSheet.materialVersion)
|
|
48
|
+
|
|
34
49
|
android {
|
|
35
50
|
namespace "com.unifiedactionsheet"
|
|
36
51
|
|
|
@@ -40,6 +55,15 @@ android {
|
|
|
40
55
|
minSdkVersion getExtOrDefault("minSdkVersion")
|
|
41
56
|
}
|
|
42
57
|
|
|
58
|
+
sourceSets {
|
|
59
|
+
main {
|
|
60
|
+
java.srcDirs += materialEnabled ? "src/material/java" : "src/noMaterial/java"
|
|
61
|
+
if (materialEnabled) {
|
|
62
|
+
res.srcDirs += "src/material/res"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
compileOptions {
|
|
44
68
|
sourceCompatibility JavaVersion.VERSION_17
|
|
45
69
|
targetCompatibility JavaVersion.VERSION_17
|
|
@@ -48,4 +72,7 @@ android {
|
|
|
48
72
|
|
|
49
73
|
dependencies {
|
|
50
74
|
implementation "com.facebook.react:react-android"
|
|
75
|
+
if (materialEnabled) {
|
|
76
|
+
implementation "com.google.android.material:material:${materialVersion}"
|
|
77
|
+
}
|
|
51
78
|
}
|
|
@@ -6,11 +6,15 @@ import com.facebook.react.bridge.ReadableMap
|
|
|
6
6
|
internal enum class PresentationStyle {
|
|
7
7
|
CENTERED,
|
|
8
8
|
ANCHORED,
|
|
9
|
+
BOTTOM,
|
|
9
10
|
;
|
|
10
11
|
|
|
11
12
|
companion object {
|
|
12
|
-
fun fromWire(value: String?): PresentationStyle =
|
|
13
|
-
|
|
13
|
+
fun fromWire(value: String?): PresentationStyle = when (value) {
|
|
14
|
+
"anchored" -> ANCHORED
|
|
15
|
+
"bottom" -> BOTTOM
|
|
16
|
+
else -> CENTERED
|
|
17
|
+
}
|
|
14
18
|
}
|
|
15
19
|
}
|
|
16
20
|
|
|
@@ -36,6 +40,24 @@ internal enum class AnchorAlignment {
|
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
/// A bottom sheet's resting height: the content, half the screen, or the full
|
|
44
|
+
/// height below the status bar.
|
|
45
|
+
internal enum class Detent {
|
|
46
|
+
AUTO,
|
|
47
|
+
MEDIUM,
|
|
48
|
+
LARGE,
|
|
49
|
+
;
|
|
50
|
+
|
|
51
|
+
companion object {
|
|
52
|
+
fun fromWire(value: String?): Detent? = when (value) {
|
|
53
|
+
"auto" -> AUTO
|
|
54
|
+
"medium" -> MEDIUM
|
|
55
|
+
"large" -> LARGE
|
|
56
|
+
else -> null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
internal enum class ForcedAppearance {
|
|
40
62
|
SYSTEM,
|
|
41
63
|
LIGHT,
|
|
@@ -66,6 +88,10 @@ internal data class ActionSheetOptions(
|
|
|
66
88
|
val presentationStyle: PresentationStyle,
|
|
67
89
|
val anchorRect: Rect?,
|
|
68
90
|
val anchorAlignment: AnchorAlignment,
|
|
91
|
+
/// Bold, as the default action. Only the first preferred button counts.
|
|
92
|
+
val preferredButtonIndex: Int? = null,
|
|
93
|
+
/// 'bottom' only, in the caller's order: the sheet opens at the first.
|
|
94
|
+
val detents: List<Detent> = emptyList(),
|
|
69
95
|
) {
|
|
70
96
|
companion object {
|
|
71
97
|
/// density converts the anchor rect: measureInWindow reports dp, while
|
|
@@ -107,9 +133,22 @@ internal data class ActionSheetOptions(
|
|
|
107
133
|
presentationStyle = PresentationStyle.fromWire(optString(map, "presentationStyle")),
|
|
108
134
|
anchorRect = optRect(map, "anchorRect", density),
|
|
109
135
|
anchorAlignment = AnchorAlignment.fromWire(optString(map, "anchorAlignment")),
|
|
136
|
+
preferredButtonIndex = optInt(map, "preferredButtonIndex"),
|
|
137
|
+
detents = optDetents(map),
|
|
110
138
|
)
|
|
111
139
|
}
|
|
112
140
|
|
|
141
|
+
private fun optDetents(map: ReadableMap): List<Detent> {
|
|
142
|
+
val detents = mutableListOf<Detent>()
|
|
143
|
+
map.getArray("detents")?.let { array ->
|
|
144
|
+
for (index in 0 until array.size()) {
|
|
145
|
+
Detent.fromWire(array.getString(index))?.let(detents::add)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return detents.distinct()
|
|
150
|
+
}
|
|
151
|
+
|
|
113
152
|
private fun optRect(map: ReadableMap, key: String, density: Float): Rect? {
|
|
114
153
|
if (!map.hasKey(key) || map.isNull(key)) return null
|
|
115
154
|
val rect = map.getMap(key) ?: return null
|
|
@@ -43,6 +43,23 @@ internal enum class PromptKeyboardType {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/// As in React Native's Alert.prompt. LOGIN_PASSWORD adds a secure field below
|
|
47
|
+
/// the first.
|
|
48
|
+
internal enum class PromptType {
|
|
49
|
+
PLAIN_TEXT,
|
|
50
|
+
SECURE_TEXT,
|
|
51
|
+
LOGIN_PASSWORD,
|
|
52
|
+
;
|
|
53
|
+
|
|
54
|
+
companion object {
|
|
55
|
+
fun fromWire(value: String?): PromptType = when (value) {
|
|
56
|
+
"secure-text" -> SECURE_TEXT
|
|
57
|
+
"login-password" -> LOGIN_PASSWORD
|
|
58
|
+
else -> PLAIN_TEXT
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
46
63
|
internal data class PromptOptions(
|
|
47
64
|
val options: List<String>,
|
|
48
65
|
val cancelButtonIndex: Int?,
|
|
@@ -50,10 +67,14 @@ internal data class PromptOptions(
|
|
|
50
67
|
val disabledButtonIndices: Set<Int>,
|
|
51
68
|
val title: String?,
|
|
52
69
|
val message: String?,
|
|
70
|
+
val type: PromptType,
|
|
53
71
|
val placeholder: String?,
|
|
72
|
+
val passwordPlaceholder: String?,
|
|
54
73
|
val defaultValue: String?,
|
|
55
74
|
val keyboardType: PromptKeyboardType,
|
|
56
|
-
val
|
|
75
|
+
val preferredButtonIndex: Int?,
|
|
76
|
+
/// Buttons kept disabled while any field is empty.
|
|
77
|
+
val textRequiredButtonIndices: Set<Int>,
|
|
57
78
|
val tintColor: String?,
|
|
58
79
|
val cancelButtonTintColor: String?,
|
|
59
80
|
val destructiveColor: String?,
|
|
@@ -79,6 +100,7 @@ internal data class PromptOptions(
|
|
|
79
100
|
presentationStyle = PresentationStyle.CENTERED,
|
|
80
101
|
anchorRect = null,
|
|
81
102
|
anchorAlignment = AnchorAlignment.START,
|
|
103
|
+
preferredButtonIndex = preferredButtonIndex,
|
|
82
104
|
)
|
|
83
105
|
|
|
84
106
|
companion object {
|
|
@@ -97,10 +119,13 @@ internal data class PromptOptions(
|
|
|
97
119
|
disabledButtonIndices = optIndices(map, "disabledButtonIndices"),
|
|
98
120
|
title = optString(map, "title"),
|
|
99
121
|
message = optString(map, "message"),
|
|
122
|
+
type = PromptType.fromWire(optString(map, "type")),
|
|
100
123
|
placeholder = optString(map, "placeholder"),
|
|
124
|
+
passwordPlaceholder = optString(map, "passwordPlaceholder"),
|
|
101
125
|
defaultValue = optString(map, "defaultValue"),
|
|
102
126
|
keyboardType = PromptKeyboardType.fromWire(optString(map, "keyboardType")),
|
|
103
|
-
|
|
127
|
+
preferredButtonIndex = optInt(map, "preferredButtonIndex"),
|
|
128
|
+
textRequiredButtonIndices = optIndices(map, "textRequiredButtonIndices"),
|
|
104
129
|
tintColor = optString(map, "tintColor"),
|
|
105
130
|
cancelButtonTintColor = optString(map, "cancelButtonTintColor"),
|
|
106
131
|
destructiveColor = optString(map, "destructiveColor"),
|
|
@@ -123,9 +148,6 @@ internal data class PromptOptions(
|
|
|
123
148
|
private fun optInt(map: ReadableMap, key: String): Int? =
|
|
124
149
|
if (map.hasKey(key) && !map.isNull(key)) map.getInt(key) else null
|
|
125
150
|
|
|
126
|
-
private fun optBoolean(map: ReadableMap, key: String): Boolean =
|
|
127
|
-
map.hasKey(key) && !map.isNull(key) && map.getBoolean(key)
|
|
128
|
-
|
|
129
151
|
private fun optString(map: ReadableMap, key: String): String? =
|
|
130
152
|
if (map.hasKey(key) && !map.isNull(key)) map.getString(key) else null
|
|
131
153
|
}
|