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.
Files changed (51) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +44 -112
  3. package/android/build.gradle +28 -1
  4. package/android/src/main/java/com/unifiedactionsheet/ActionSheetOptions.kt +41 -2
  5. package/android/src/main/java/com/unifiedactionsheet/PromptOptions.kt +27 -5
  6. package/android/src/main/java/com/unifiedactionsheet/SheetContentBuilder.kt +75 -9
  7. package/android/src/main/java/com/unifiedactionsheet/SheetPresenters.kt +61 -10
  8. package/android/src/main/java/com/unifiedactionsheet/UnifiedActionSheetModule.kt +14 -7
  9. package/android/src/material/java/com/unifiedactionsheet/BottomSheetPresenterFactory.kt +10 -0
  10. package/android/src/material/java/com/unifiedactionsheet/MaterialBottomSheetPresenter.kt +153 -0
  11. package/android/src/material/res/values/unified_action_sheet_bottom_sheet.xml +22 -0
  12. package/android/src/noMaterial/java/com/unifiedactionsheet/BottomSheetPresenterFactory.kt +10 -0
  13. package/app.plugin.js +1 -0
  14. package/ios/BottomSheetViewController.swift +210 -0
  15. package/ios/UnifiedActionSheet.mm +51 -5
  16. package/ios/UnifiedActionSheetImpl.swift +259 -22
  17. package/jest/index.d.ts +12 -9
  18. package/jest/index.js +37 -18
  19. package/lib/commonjs/NativeUnifiedActionSheet.js.map +1 -1
  20. package/lib/commonjs/index.js +102 -18
  21. package/lib/commonjs/index.js.map +1 -1
  22. package/lib/module/NativeUnifiedActionSheet.js.map +1 -1
  23. package/lib/module/index.js +102 -18
  24. package/lib/module/index.js.map +1 -1
  25. package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts +10 -1
  26. package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts +5 -2
  28. package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts.map +1 -1
  29. package/lib/typescript/commonjs/src/common-options.interface.d.ts +9 -0
  30. package/lib/typescript/commonjs/src/common-options.interface.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/src/index.d.ts +6 -6
  32. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/src/prompt-options.interface.d.ts +10 -6
  34. package/lib/typescript/commonjs/src/prompt-options.interface.d.ts.map +1 -1
  35. package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts +10 -1
  36. package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts.map +1 -1
  37. package/lib/typescript/module/src/action-sheet-options.interface.d.ts +5 -2
  38. package/lib/typescript/module/src/action-sheet-options.interface.d.ts.map +1 -1
  39. package/lib/typescript/module/src/common-options.interface.d.ts +9 -0
  40. package/lib/typescript/module/src/common-options.interface.d.ts.map +1 -1
  41. package/lib/typescript/module/src/index.d.ts +6 -6
  42. package/lib/typescript/module/src/index.d.ts.map +1 -1
  43. package/lib/typescript/module/src/prompt-options.interface.d.ts +10 -6
  44. package/lib/typescript/module/src/prompt-options.interface.d.ts.map +1 -1
  45. package/package.json +12 -2
  46. package/plugin/withUnifiedActionSheet.js +58 -0
  47. package/src/NativeUnifiedActionSheet.ts +12 -2
  48. package/src/action-sheet-options.interface.ts +12 -1
  49. package/src/common-options.interface.ts +18 -0
  50. package/src/index.tsx +147 -42
  51. 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.2.0...HEAD
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 API for React Native: a native `UIAlertController` on iOS, a native `AppCompatDialog` on Android.
3
+ Unified native action sheet for React Native, one API on iOS and Android.
4
4
 
5
- | Android | Android (dark mode) | iOS |
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 dark-mode demo — following the system dark theme" /> | <img src="https://raw.githubusercontent.com/galikvalkin/react-native-unified-action-sheet/HEAD/docs/ios-demo.gif" width="280" alt="iOS demo" /> |
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
- <p align="center">
10
- <img src="https://raw.githubusercontent.com/galikvalkin/react-native-unified-action-sheet/HEAD/docs/ipad-demo.gif" width="420" alt="iPad demo — the sheet as a popover anchored to the button that opened it" />
11
- <br />
12
- <em>iPad: the same sheet presented as a popover, anchored to the view that opened it.</em>
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 on both platforms. In Expo apps, use a development build (`npx expo run:ios` / `npx expo run:android`) — custom native modules do not work in **Expo Go**.
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
- `style` marks the two native roles, which are mutually exclusive; `disabled` is independent of it. `onPress` runs when that button resolves the sheet, and the call also resolves with the tapped index — the two never disagree:
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
- <Pressable
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
- Anything with a `measureInWindow` method works, which is every React Native host component ref. On iOS the anchor only applies on iPad, where the sheet becomes a popover; iPhone always presents from the bottom.
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 }, or undefined after dismissActionSheet().
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
- ### Options
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
- jest.mock('react-native-unified-action-sheet', () =>
154
- require('react-native-unified-action-sheet/jest')
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
- Every sheet then resolves with no selection. To simulate a tap, queue the index it should resolve with — the pressed button's `onPress` runs as it would for real:
92
+ ### Dismissing from code
159
93
 
160
- ```ts
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
- setNextButtonIndex(0);
164
- await openTheSheet();
165
- ```
96
+ ## Documentation
166
97
 
167
- `setNextPromptResult({ buttonIndex, text })` does the same for a prompt. Prefer
168
- these over `mockResolvedValueOnce`, which replaces the implementation and so
169
- skips `onPress`. `dismissActionSheet` and `dismissAllActionSheets` are spies.
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
 
@@ -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
- if (value == "anchored") ANCHORED else CENTERED
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 secureTextEntry: Boolean,
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
- secureTextEntry = optBoolean(map, "secureTextEntry"),
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
  }