react-native-unified-action-sheet 0.1.1 → 0.3.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 +66 -1
- package/README.md +38 -105
- package/android/build.gradle +28 -1
- package/android/src/main/java/com/unifiedactionsheet/ActionSheetOptions.kt +6 -2
- package/android/src/main/java/com/unifiedactionsheet/PromptOptions.kt +132 -0
- package/android/src/main/java/com/unifiedactionsheet/SheetContentBuilder.kt +31 -0
- package/android/src/main/java/com/unifiedactionsheet/SheetPresenters.kt +54 -0
- package/android/src/main/java/com/unifiedactionsheet/UnifiedActionSheetModule.kt +61 -0
- package/android/src/material/java/com/unifiedactionsheet/BottomSheetPresenterFactory.kt +10 -0
- package/android/src/material/java/com/unifiedactionsheet/MaterialBottomSheetPresenter.kt +90 -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 +208 -0
- package/ios/UnifiedActionSheet.mm +73 -1
- package/ios/UnifiedActionSheetImpl.swift +248 -11
- package/jest/index.d.ts +15 -1
- package/jest/index.js +28 -0
- package/lib/commonjs/NativeUnifiedActionSheet.js.map +1 -1
- package/lib/commonjs/action-sheet-options.interface.js +4 -0
- package/lib/commonjs/common-options.interface.js +2 -0
- package/lib/commonjs/common-options.interface.js.map +1 -0
- package/lib/commonjs/index.js +57 -5
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/prompt-options.interface.js +6 -0
- package/lib/commonjs/prompt-options.interface.js.map +1 -0
- package/lib/module/NativeUnifiedActionSheet.js.map +1 -1
- package/lib/module/action-sheet-options.interface.js +2 -0
- package/lib/module/common-options.interface.js +2 -0
- package/lib/module/common-options.interface.js.map +1 -0
- package/lib/module/index.js +55 -4
- package/lib/module/index.js.map +1 -1
- package/lib/module/prompt-options.interface.js +4 -0
- package/lib/module/prompt-options.interface.js.map +1 -0
- package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts +23 -0
- package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts +5 -14
- package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/common-options.interface.d.ts +17 -0
- package/lib/typescript/commonjs/src/common-options.interface.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +4 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/prompt-options.interface.d.ts +19 -0
- package/lib/typescript/commonjs/src/prompt-options.interface.d.ts.map +1 -0
- package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts +23 -0
- package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts.map +1 -1
- package/lib/typescript/module/src/action-sheet-options.interface.d.ts +5 -14
- package/lib/typescript/module/src/action-sheet-options.interface.d.ts.map +1 -1
- package/lib/typescript/module/src/common-options.interface.d.ts +17 -0
- package/lib/typescript/module/src/common-options.interface.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +4 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/prompt-options.interface.d.ts +19 -0
- package/lib/typescript/module/src/prompt-options.interface.d.ts.map +1 -0
- package/package.json +12 -2
- package/plugin/withUnifiedActionSheet.js +58 -0
- package/src/NativeUnifiedActionSheet.ts +22 -0
- package/src/action-sheet-options.interface.ts +10 -20
- package/src/common-options.interface.ts +18 -0
- package/src/index.tsx +100 -6
- package/src/prompt-options.interface.ts +27 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0]
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `presentationStyle: 'bottom'`: a native bottom sheet on both platforms, with
|
|
15
|
+
a drag handle and swipe to dismiss. A long list opens part-way and drags up
|
|
16
|
+
to expand. On iOS it is a `UISheetPresentationController` sheet with native
|
|
17
|
+
rows (on iPad, the action sheet's popover instead); on Android, a Material 3
|
|
18
|
+
bottom sheet. `anchor` is ignored for it.
|
|
19
|
+
- Material is opt-in, so apps that don't use the bottom sheet stay free of the
|
|
20
|
+
dependency. Set `unifiedActionSheet.material=true` in
|
|
21
|
+
`android/gradle.properties`. Only then does the library add
|
|
22
|
+
`com.google.android.material` (1.12.0 by default; override it with
|
|
23
|
+
`ext.materialVersion` or `unifiedActionSheet.materialVersion`).
|
|
24
|
+
- An Expo config plugin that writes those properties:
|
|
25
|
+
`plugins: [['react-native-unified-action-sheet', { material: true }]]`, with
|
|
26
|
+
an optional `materialVersion`. `@expo/config-plugins` is an optional peer
|
|
27
|
+
dependency.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- On iOS, a sheet, alert or prompt stayed on screen after a JS reload (pressing
|
|
32
|
+
`r` in Metro). It now closes on reload, as it already did on Android, and its
|
|
33
|
+
promise is left unresolved, since it belonged to the discarded runtime.
|
|
34
|
+
|
|
35
|
+
### Notes
|
|
36
|
+
|
|
37
|
+
- Without the Material opt-in, `'bottom'` on Android falls back to the centered
|
|
38
|
+
dialog, and warns once in development.
|
|
39
|
+
- Prompts do not support `'bottom'`; they stay centered on both platforms.
|
|
40
|
+
|
|
41
|
+
## [0.2.0]
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- `showPromptWithOptions(options)` — the same native dialog as the action sheet,
|
|
46
|
+
with a text field. React Native's own `Alert.prompt` is iOS-only and silently
|
|
47
|
+
does nothing on Android, so this is the one API here with no core equivalent.
|
|
48
|
+
Resolves `{ buttonIndex, text }`, or `undefined` after `dismissActionSheet()`.
|
|
49
|
+
- Prompt button handlers receive the field's text. It is read when the prompt
|
|
50
|
+
closes, including on a dismissal, so a draft is recoverable rather than lost.
|
|
51
|
+
- Prompt options: `placeholder`, `defaultValue`, `secureTextEntry` and
|
|
52
|
+
`keyboardType` (`'default' | 'email-address' | 'numeric' | 'phone-pad' |
|
|
53
|
+
'url'`). Buttons keep the sheet's `style`, `disabled` and `onPress`.
|
|
54
|
+
- `setNextPromptResult({ buttonIndex, text })` in the shipped Jest mock, the
|
|
55
|
+
prompt counterpart to `setNextButtonIndex()`.
|
|
56
|
+
- Exported types: `PromptButtonInterface`, `PromptCommonOptionsInterface`,
|
|
57
|
+
`PromptAndroidOptionsInterface`, `PromptOptionsInterface`,
|
|
58
|
+
`PromptResultInterface`, plus the shared `BaseButtonInterface`,
|
|
59
|
+
`BaseOptionsInterface` and `BaseAndroidOptionsInterface`.
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- The options and button fields the sheet and the prompt share are now declared
|
|
64
|
+
once, in `BaseOptionsInterface` / `BaseButtonInterface`, which both APIs
|
|
65
|
+
extend. No public type changed shape.
|
|
66
|
+
|
|
67
|
+
### Notes
|
|
68
|
+
|
|
69
|
+
- A prompt is always presented centered. UIKit has no text field in an action
|
|
70
|
+
sheet, so `presentationStyle` and `anchor` do not apply to one.
|
|
71
|
+
- `dismissActionSheet()` and `dismissAllActionSheets()` close prompts too.
|
|
72
|
+
|
|
10
73
|
## [0.1.1]
|
|
11
74
|
|
|
12
75
|
No user-facing changes — the library is identical to 0.1.0. Republished from CI
|
|
@@ -43,6 +106,8 @@ Initial release.
|
|
|
43
106
|
- No `com.google.android.material` dependency, and no runtime dependencies —
|
|
44
107
|
`react` and `react-native` are peers.
|
|
45
108
|
|
|
46
|
-
[unreleased]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.
|
|
109
|
+
[unreleased]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.3.0...HEAD
|
|
110
|
+
[0.3.0]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.2.0...v0.3.0
|
|
111
|
+
[0.2.0]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.1.1...v0.2.0
|
|
47
112
|
[0.1.1]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.1.0...v0.1.1
|
|
48
113
|
[0.1.0]: https://github.com/galikvalkin/react-native-unified-action-sheet/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -1,21 +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 entire React Native view tree — including above an already-open [`Modal`](https://reactnative.dev/docs/modal). JS-rendered action 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
|
-
- Works on React Native **0.81 (old and new architecture)** through the **latest** release.
|
|
18
|
-
- **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.
|
|
19
14
|
|
|
20
15
|
## Installation
|
|
21
16
|
|
|
@@ -25,7 +20,9 @@ npm install react-native-unified-action-sheet
|
|
|
25
20
|
yarn add react-native-unified-action-sheet
|
|
26
21
|
```
|
|
27
22
|
|
|
28
|
-
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).
|
|
29
26
|
|
|
30
27
|
## Usage
|
|
31
28
|
|
|
@@ -44,108 +41,44 @@ await showActionSheetWithOptions({
|
|
|
44
41
|
});
|
|
45
42
|
```
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
| | promise resolves with | `onPress` runs |
|
|
50
|
-
| --- | --- | --- |
|
|
51
|
-
| Tapping a button | its index | that button's |
|
|
52
|
-
| 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 |
|
|
53
|
-
| `dismissActionSheet()` | `undefined` | nothing |
|
|
54
|
-
|
|
55
|
-
The promise never rejects on its own — only if one of your own `onPress` handlers throws.
|
|
56
|
-
|
|
57
|
-
### Anchored sheets
|
|
44
|
+
The call resolves with the tapped button's index, and that button's `onPress` runs. A backdrop tap or back resolves the `'cancel'` button. The promise never rejects. See [Behavior](docs/behavior.md).
|
|
58
45
|
|
|
59
|
-
|
|
46
|
+
### Presentation styles
|
|
60
47
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
showActionSheetWithOptions({
|
|
68
|
-
options: [...],
|
|
69
|
-
presentationStyle: 'anchored',
|
|
70
|
-
anchor: anchorRef,
|
|
71
|
-
})
|
|
72
|
-
}
|
|
73
|
-
/>;
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
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.
|
|
77
|
-
|
|
78
|
-
### Dismissing from code
|
|
79
|
-
|
|
80
|
-
`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`.
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
import {
|
|
84
|
-
dismissActionSheet,
|
|
85
|
-
dismissAllActionSheets,
|
|
86
|
-
} from 'react-native-unified-action-sheet';
|
|
87
|
-
```
|
|
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)) |
|
|
88
54
|
|
|
89
|
-
###
|
|
90
|
-
|
|
91
|
-
| Option | Type | iOS | Android | Description |
|
|
92
|
-
| --- | --- | :---: | :---: | --- |
|
|
93
|
-
| `options` | `ActionSheetButtonInterface[]` | ✅ | ✅ | The buttons, in order: `{ label, style?, disabled?, onPress? }`. |
|
|
94
|
-
| `style` (per button) | `'cancel' \| 'destructive'?` | ✅ | ✅ | `'cancel'` renders a separated cancel row on Android and resolves on backdrop tap / back; `'destructive'` renders in the destructive color. Only the first `'cancel'` counts. |
|
|
95
|
-
| `disabled` (per button) | `boolean?` | ✅ | ✅ | Renders the row dimmed and unresponsive to taps. |
|
|
96
|
-
| `onPress` (per button) | `() => void?` | ✅ | ✅ | Runs when this button resolves the sheet. |
|
|
97
|
-
| `title` | `string?` | ✅ | ✅ | Sheet title. |
|
|
98
|
-
| `message` | `string?` | ✅ | ✅ | Secondary text under the title. |
|
|
99
|
-
| `presentationStyle` | `'centered' \| 'anchored'` | ✅ | ✅ | `'centered'` is a centered dialog; `'anchored'` attaches to `anchor` (an iPad popover on iOS). **Defaults differ**: Android defaults to `'centered'`, iOS to the standard action sheet. |
|
|
100
|
-
| `anchor` | `ActionSheetAnchorInterface?` | ✅ | ✅ | The view to attach to — a ref, or anything with `measureInWindow`. Without a measurable anchor an `'anchored'` sheet falls back to a centered dialog. |
|
|
101
|
-
| `anchorAlignment` | `'start' \| 'center'` | — | ✅ | Alignment of an `'anchored'` popup relative to its anchor. `'start'` (default) aligns leading edges, flipping in RTL. |
|
|
102
|
-
| `userInterfaceStyle` | `'light' \| 'dark'` | ✅ | ✅ | Forces the appearance; defaults to following the system setting. |
|
|
103
|
-
| `tintColor` | `string?` | ✅ | ✅ | Text color of non-destructive buttons. |
|
|
104
|
-
| `cancelButtonTintColor` | `string?` | ✅ | ✅ | Text color of the cancel button; overrides `tintColor` for that row. |
|
|
105
|
-
| `destructiveColor` | `string?` | ✅ | ✅ | Overrides the destructive row color (Android's palette error color, iOS system red). |
|
|
106
|
-
| `buttonTextAlignment` | `'start' \| 'center'` | — | ✅ | Alignment of button labels. Defaults to `'start'`, which follows layout direction. |
|
|
107
|
-
|
|
108
|
-
The options types are exported for typing your own wrappers:
|
|
55
|
+
### Prompts
|
|
109
56
|
|
|
110
57
|
```ts
|
|
111
|
-
import
|
|
112
|
-
ActionSheetOptionsInterface, // what showActionSheetWithOptions accepts
|
|
113
|
-
ActionSheetCommonOptionsInterface,
|
|
114
|
-
ActionSheetAndroidOptionsInterface,
|
|
115
|
-
ActionSheetButtonInterface, // a single button
|
|
116
|
-
ActionSheetAnchorInterface, // anything with measureInWindow
|
|
117
|
-
} from 'react-native-unified-action-sheet';
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### Platform notes
|
|
121
|
-
|
|
122
|
-
- **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.
|
|
123
|
-
- **On iPad, a popover hides the cancel row**, since tapping outside already cancels. The index you receive is unaffected.
|
|
124
|
-
- **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.
|
|
125
|
-
- **Light or dark follows the system setting** unless `userInterfaceStyle` forces one, chosen when the sheet opens. The sheet uses its own palette, so it looks the same in any host app.
|
|
126
|
-
- **Long lists scroll**, with the title and message pinned above them.
|
|
127
|
-
- On Android, a sheet with no available activity resolves the cancel index rather than hanging, so a cancel handler can run for a sheet that never appeared.
|
|
58
|
+
import { showPromptWithOptions } from 'react-native-unified-action-sheet';
|
|
128
59
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
60
|
+
const result = await showPromptWithOptions({
|
|
61
|
+
title: 'Rename item',
|
|
62
|
+
placeholder: 'New name',
|
|
63
|
+
defaultValue: 'Untitled',
|
|
64
|
+
options: [
|
|
65
|
+
{ label: 'Save', onPress: (text) => rename(text) },
|
|
66
|
+
{ label: 'Cancel', style: 'cancel' },
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
// { buttonIndex, text }, or undefined after dismissActionSheet().
|
|
137
70
|
```
|
|
138
71
|
|
|
139
|
-
|
|
72
|
+
### Dismissing from code
|
|
140
73
|
|
|
141
|
-
|
|
142
|
-
import { setNextButtonIndex } from 'react-native-unified-action-sheet/jest';
|
|
74
|
+
`dismissActionSheet()` closes the most recently opened sheet, and `dismissAllActionSheets()` closes every open one. Dismissed sheets resolve with `undefined`.
|
|
143
75
|
|
|
144
|
-
|
|
145
|
-
await openTheSheet();
|
|
146
|
-
```
|
|
76
|
+
## Documentation
|
|
147
77
|
|
|
148
|
-
|
|
78
|
+
- [API reference](docs/api.md): every option, per platform
|
|
79
|
+
- [Behavior](docs/behavior.md): what each gesture resolves with, and platform differences
|
|
80
|
+
- [Bottom sheets](docs/bottom-sheet.md): both platforms, and enabling Material on Android
|
|
81
|
+
- [Testing](docs/testing.md): the shipped Jest mock
|
|
149
82
|
|
|
150
83
|
## Compatibility
|
|
151
84
|
|
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
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
package com.unifiedactionsheet
|
|
2
|
+
|
|
3
|
+
import android.text.InputType
|
|
4
|
+
import com.facebook.react.bridge.ReadableMap
|
|
5
|
+
|
|
6
|
+
/// The subset of React Native's keyboardType values that map onto both
|
|
7
|
+
/// platforms. Anything unrecognised falls back to the plain text keyboard.
|
|
8
|
+
internal enum class PromptKeyboardType {
|
|
9
|
+
DEFAULT,
|
|
10
|
+
EMAIL_ADDRESS,
|
|
11
|
+
NUMERIC,
|
|
12
|
+
PHONE_PAD,
|
|
13
|
+
URL,
|
|
14
|
+
;
|
|
15
|
+
|
|
16
|
+
fun toInputType(secure: Boolean): Int {
|
|
17
|
+
if (secure) {
|
|
18
|
+
// Keep the keyboard class: masking a passcode should still show digits.
|
|
19
|
+
return when (this) {
|
|
20
|
+
NUMERIC, PHONE_PAD ->
|
|
21
|
+
InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
|
|
22
|
+
else -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return when (this) {
|
|
27
|
+
DEFAULT -> InputType.TYPE_CLASS_TEXT
|
|
28
|
+
EMAIL_ADDRESS -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
|
29
|
+
NUMERIC -> InputType.TYPE_CLASS_NUMBER
|
|
30
|
+
PHONE_PAD -> InputType.TYPE_CLASS_PHONE
|
|
31
|
+
URL -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
companion object {
|
|
36
|
+
fun fromWire(value: String?): PromptKeyboardType = when (value) {
|
|
37
|
+
"email-address" -> EMAIL_ADDRESS
|
|
38
|
+
"numeric" -> NUMERIC
|
|
39
|
+
"phone-pad" -> PHONE_PAD
|
|
40
|
+
"url" -> URL
|
|
41
|
+
else -> DEFAULT
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
internal data class PromptOptions(
|
|
47
|
+
val options: List<String>,
|
|
48
|
+
val cancelButtonIndex: Int?,
|
|
49
|
+
val destructiveButtonIndices: Set<Int>,
|
|
50
|
+
val disabledButtonIndices: Set<Int>,
|
|
51
|
+
val title: String?,
|
|
52
|
+
val message: String?,
|
|
53
|
+
val placeholder: String?,
|
|
54
|
+
val defaultValue: String?,
|
|
55
|
+
val keyboardType: PromptKeyboardType,
|
|
56
|
+
val secureTextEntry: Boolean,
|
|
57
|
+
val tintColor: String?,
|
|
58
|
+
val cancelButtonTintColor: String?,
|
|
59
|
+
val destructiveColor: String?,
|
|
60
|
+
val buttonTextAlignment: ButtonTextAlignment,
|
|
61
|
+
val userInterfaceStyle: ForcedAppearance,
|
|
62
|
+
) {
|
|
63
|
+
/// The prompt reuses the sheet's content and dialog code, which are written
|
|
64
|
+
/// against ActionSheetOptions. Presentation is fixed to centered: UIKit has
|
|
65
|
+
/// no text field in an action sheet, so an anchored prompt has no iOS
|
|
66
|
+
/// counterpart and would break the "same options behave the same way" rule.
|
|
67
|
+
fun toSheetOptions(): ActionSheetOptions = ActionSheetOptions(
|
|
68
|
+
options = options,
|
|
69
|
+
cancelButtonIndex = cancelButtonIndex,
|
|
70
|
+
destructiveButtonIndices = destructiveButtonIndices,
|
|
71
|
+
title = title,
|
|
72
|
+
message = message,
|
|
73
|
+
tintColor = tintColor,
|
|
74
|
+
cancelButtonTintColor = cancelButtonTintColor,
|
|
75
|
+
destructiveColor = destructiveColor,
|
|
76
|
+
buttonTextAlignment = buttonTextAlignment,
|
|
77
|
+
disabledButtonIndices = disabledButtonIndices,
|
|
78
|
+
userInterfaceStyle = userInterfaceStyle,
|
|
79
|
+
presentationStyle = PresentationStyle.CENTERED,
|
|
80
|
+
anchorRect = null,
|
|
81
|
+
anchorAlignment = AnchorAlignment.START,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
companion object {
|
|
85
|
+
fun fromReadableMap(map: ReadableMap): PromptOptions {
|
|
86
|
+
val labels = mutableListOf<String>()
|
|
87
|
+
map.getArray("options")?.let { array ->
|
|
88
|
+
for (index in 0 until array.size()) {
|
|
89
|
+
array.getString(index)?.let(labels::add)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return PromptOptions(
|
|
94
|
+
options = labels,
|
|
95
|
+
cancelButtonIndex = optInt(map, "cancelButtonIndex"),
|
|
96
|
+
destructiveButtonIndices = optIndices(map, "destructiveButtonIndices"),
|
|
97
|
+
disabledButtonIndices = optIndices(map, "disabledButtonIndices"),
|
|
98
|
+
title = optString(map, "title"),
|
|
99
|
+
message = optString(map, "message"),
|
|
100
|
+
placeholder = optString(map, "placeholder"),
|
|
101
|
+
defaultValue = optString(map, "defaultValue"),
|
|
102
|
+
keyboardType = PromptKeyboardType.fromWire(optString(map, "keyboardType")),
|
|
103
|
+
secureTextEntry = optBoolean(map, "secureTextEntry"),
|
|
104
|
+
tintColor = optString(map, "tintColor"),
|
|
105
|
+
cancelButtonTintColor = optString(map, "cancelButtonTintColor"),
|
|
106
|
+
destructiveColor = optString(map, "destructiveColor"),
|
|
107
|
+
buttonTextAlignment = ButtonTextAlignment.fromWire(optString(map, "buttonTextAlignment")),
|
|
108
|
+
userInterfaceStyle = ForcedAppearance.fromWire(optString(map, "userInterfaceStyle")),
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private fun optIndices(map: ReadableMap, key: String): Set<Int> {
|
|
113
|
+
val indices = mutableSetOf<Int>()
|
|
114
|
+
map.getArray(key)?.let { array ->
|
|
115
|
+
for (index in 0 until array.size()) {
|
|
116
|
+
indices.add(array.getInt(index))
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return indices
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private fun optInt(map: ReadableMap, key: String): Int? =
|
|
124
|
+
if (map.hasKey(key) && !map.isNull(key)) map.getInt(key) else null
|
|
125
|
+
|
|
126
|
+
private fun optBoolean(map: ReadableMap, key: String): Boolean =
|
|
127
|
+
map.hasKey(key) && !map.isNull(key) && map.getBoolean(key)
|
|
128
|
+
|
|
129
|
+
private fun optString(map: ReadableMap, key: String): String? =
|
|
130
|
+
if (map.hasKey(key) && !map.isNull(key)) map.getString(key) else null
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -6,9 +6,11 @@ import android.util.TypedValue
|
|
|
6
6
|
import android.view.Gravity
|
|
7
7
|
import android.view.View
|
|
8
8
|
import android.widget.Button
|
|
9
|
+
import android.widget.EditText
|
|
9
10
|
import android.widget.LinearLayout
|
|
10
11
|
import android.widget.TextView
|
|
11
12
|
import androidx.appcompat.content.res.AppCompatResources
|
|
13
|
+
import androidx.appcompat.widget.AppCompatEditText
|
|
12
14
|
import androidx.core.graphics.ColorUtils
|
|
13
15
|
import androidx.core.view.AccessibilityDelegateCompat
|
|
14
16
|
import androidx.core.view.ViewCompat
|
|
@@ -22,6 +24,8 @@ internal fun buildContent(
|
|
|
22
24
|
options: ActionSheetOptions,
|
|
23
25
|
palette: SheetPalette,
|
|
24
26
|
includeCancelRow: Boolean = true,
|
|
27
|
+
/// Sits between the message and the rows. Only the prompt supplies one.
|
|
28
|
+
inputView: View? = null,
|
|
25
29
|
onSelect: (Int) -> Unit,
|
|
26
30
|
): LinearLayout {
|
|
27
31
|
val container = LinearLayout(context).apply {
|
|
@@ -36,6 +40,8 @@ internal fun buildContent(
|
|
|
36
40
|
container.addView(buildHeader(context, it, palette.secondaryText, isTitle = false))
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
inputView?.let(container::addView)
|
|
44
|
+
|
|
39
45
|
val centerLabels = options.buttonTextAlignment == ButtonTextAlignment.CENTER
|
|
40
46
|
|
|
41
47
|
val optionColor = parseColor(options.tintColor) ?: palette.primaryText
|
|
@@ -96,6 +102,31 @@ internal fun buildContent(
|
|
|
96
102
|
return container
|
|
97
103
|
}
|
|
98
104
|
|
|
105
|
+
internal fun buildPromptInput(
|
|
106
|
+
context: Context,
|
|
107
|
+
options: PromptOptions,
|
|
108
|
+
palette: SheetPalette,
|
|
109
|
+
): EditText = AppCompatEditText(context).apply {
|
|
110
|
+
setText(options.defaultValue.orEmpty())
|
|
111
|
+
hint = options.placeholder
|
|
112
|
+
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
|
|
113
|
+
setTextColor(palette.primaryText)
|
|
114
|
+
setHintTextColor(ColorUtils.setAlphaComponent(palette.secondaryText, DISABLED_TEXT_ALPHA))
|
|
115
|
+
setPadding(dp(context, 16), dp(context, 8), dp(context, 16), dp(context, 8))
|
|
116
|
+
isSingleLine = true
|
|
117
|
+
// After isSingleLine, never before: setSingleLine() installs
|
|
118
|
+
// SingleLineTransformationMethod, which replaces the password masking that
|
|
119
|
+
// setInputType() sets up. Ordering them the other way renders a secure field
|
|
120
|
+
// in plain text while still reporting a password input type.
|
|
121
|
+
inputType = options.keyboardType.toInputType(options.secureTextEntry)
|
|
122
|
+
// Land the caret after any default value rather than before it.
|
|
123
|
+
setSelection(text?.length ?: 0)
|
|
124
|
+
layoutParams = LinearLayout.LayoutParams(
|
|
125
|
+
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
126
|
+
LinearLayout.LayoutParams.WRAP_CONTENT,
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
99
130
|
private fun buildHeader(
|
|
100
131
|
context: Context,
|
|
101
132
|
text: String,
|
|
@@ -83,6 +83,60 @@ internal object CenteredDialogPresenter : SheetPresenter {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/// The centered dialog with a text field above the buttons. Deliberately not a
|
|
87
|
+
/// SheetPresenter: it takes PromptOptions and hands the field's text back with
|
|
88
|
+
/// the index, which the sheet interface has no place for.
|
|
89
|
+
internal fun buildPromptDialog(
|
|
90
|
+
activity: Activity,
|
|
91
|
+
options: PromptOptions,
|
|
92
|
+
onSelect: (Dialog, Int, String) -> Unit,
|
|
93
|
+
): Pair<Dialog, () -> String> {
|
|
94
|
+
val isDark = isDarkAppearance(activity, options.userInterfaceStyle)
|
|
95
|
+
val palette = paletteFor(isDark)
|
|
96
|
+
val dialog = AppCompatDialog(activity, dialogTheme(isDark))
|
|
97
|
+
dialog.supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
|
|
98
|
+
|
|
99
|
+
val context: Context = dialog.context
|
|
100
|
+
val input = buildPromptInput(context, options, palette)
|
|
101
|
+
val currentText = { input.text?.toString().orEmpty() }
|
|
102
|
+
|
|
103
|
+
val container = buildContent(
|
|
104
|
+
context = context,
|
|
105
|
+
options = options.toSheetOptions(),
|
|
106
|
+
palette = palette,
|
|
107
|
+
inputView = input,
|
|
108
|
+
) { index -> onSelect(dialog, index, currentText()) }
|
|
109
|
+
|
|
110
|
+
dialog.setContentView(container)
|
|
111
|
+
dialog.setCanceledOnTouchOutside(true)
|
|
112
|
+
|
|
113
|
+
val background = GradientDrawable().apply {
|
|
114
|
+
cornerRadius = dp(context, PROMPT_CORNER_RADIUS_DP).toFloat()
|
|
115
|
+
setColor(palette.surface)
|
|
116
|
+
}
|
|
117
|
+
dialog.window?.setBackgroundDrawable(background)
|
|
118
|
+
container.background = background.constantState?.newDrawable() ?: background
|
|
119
|
+
container.clipToOutline = true
|
|
120
|
+
|
|
121
|
+
val metrics = context.resources.displayMetrics
|
|
122
|
+
val width = (metrics.widthPixels - 2 * dp(context, PROMPT_MARGIN_DP))
|
|
123
|
+
.coerceAtMost(dp(context, PROMPT_MAX_WIDTH_DP))
|
|
124
|
+
.coerceAtLeast(minOf(dp(context, PROMPT_MIN_WIDTH_DP), metrics.widthPixels))
|
|
125
|
+
dialog.window?.setLayout(width, WindowManager.LayoutParams.WRAP_CONTENT)
|
|
126
|
+
|
|
127
|
+
// A prompt exists to be typed into, so raise the keyboard with it rather than
|
|
128
|
+
// making the user tap the field first.
|
|
129
|
+
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
|
|
130
|
+
input.requestFocus()
|
|
131
|
+
|
|
132
|
+
return dialog to currentText
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private const val PROMPT_CORNER_RADIUS_DP = 28
|
|
136
|
+
private const val PROMPT_MARGIN_DP = 24
|
|
137
|
+
private const val PROMPT_MIN_WIDTH_DP = 280
|
|
138
|
+
private const val PROMPT_MAX_WIDTH_DP = 560
|
|
139
|
+
|
|
86
140
|
internal class AnchoredDialogPresenter(private val anchorRect: Rect) : SheetPresenter {
|
|
87
141
|
override fun build(
|
|
88
142
|
activity: Activity,
|