react-native-unified-action-sheet 0.1.0 → 0.2.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 +41 -1
- package/README.md +42 -21
- 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 +55 -0
- package/ios/UnifiedActionSheet.mm +47 -0
- package/ios/UnifiedActionSheetImpl.swift +101 -4
- 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 +39 -4
- 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 +37 -3
- 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 +20 -0
- package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts +4 -13
- 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 +20 -0
- package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts.map +1 -1
- package/lib/typescript/module/src/action-sheet-options.interface.d.ts +4 -13
- 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 +1 -1
- package/src/NativeUnifiedActionSheet.ts +17 -0
- package/src/action-sheet-options.interface.ts +9 -19
- package/src/common-options.interface.ts +18 -0
- package/src/index.tsx +65 -3
- package/src/prompt-options.interface.ts +27 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0]
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `showPromptWithOptions(options)` — the same native dialog as the action sheet,
|
|
15
|
+
with a text field. React Native's own `Alert.prompt` is iOS-only and silently
|
|
16
|
+
does nothing on Android, so this is the one API here with no core equivalent.
|
|
17
|
+
Resolves `{ buttonIndex, text }`, or `undefined` after `dismissActionSheet()`.
|
|
18
|
+
- Prompt button handlers receive the field's text. It is read when the prompt
|
|
19
|
+
closes, including on a dismissal, so a draft is recoverable rather than lost.
|
|
20
|
+
- Prompt options: `placeholder`, `defaultValue`, `secureTextEntry` and
|
|
21
|
+
`keyboardType` (`'default' | 'email-address' | 'numeric' | 'phone-pad' |
|
|
22
|
+
'url'`). Buttons keep the sheet's `style`, `disabled` and `onPress`.
|
|
23
|
+
- `setNextPromptResult({ buttonIndex, text })` in the shipped Jest mock, the
|
|
24
|
+
prompt counterpart to `setNextButtonIndex()`.
|
|
25
|
+
- Exported types: `PromptButtonInterface`, `PromptCommonOptionsInterface`,
|
|
26
|
+
`PromptAndroidOptionsInterface`, `PromptOptionsInterface`,
|
|
27
|
+
`PromptResultInterface`, plus the shared `BaseButtonInterface`,
|
|
28
|
+
`BaseOptionsInterface` and `BaseAndroidOptionsInterface`.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- The options and button fields the sheet and the prompt share are now declared
|
|
33
|
+
once, in `BaseOptionsInterface` / `BaseButtonInterface`, which both APIs
|
|
34
|
+
extend. No public type changed shape.
|
|
35
|
+
|
|
36
|
+
### Notes
|
|
37
|
+
|
|
38
|
+
- A prompt is always presented centered. UIKit has no text field in an action
|
|
39
|
+
sheet, so `presentationStyle` and `anchor` do not apply to one.
|
|
40
|
+
- `dismissActionSheet()` and `dismissAllActionSheets()` close prompts too.
|
|
41
|
+
|
|
42
|
+
## [0.1.1]
|
|
43
|
+
|
|
44
|
+
No user-facing changes — the library is identical to 0.1.0. Republished from CI
|
|
45
|
+
so the release carries npm provenance; 0.1.0 was published by hand, because npm
|
|
46
|
+
only lets a trusted publisher be configured on a package that already exists.
|
|
47
|
+
|
|
10
48
|
## [0.1.0]
|
|
11
49
|
|
|
12
50
|
Initial release.
|
|
@@ -37,5 +75,7 @@ Initial release.
|
|
|
37
75
|
- No `com.google.android.material` dependency, and no runtime dependencies —
|
|
38
76
|
`react` and `react-native` are peers.
|
|
39
77
|
|
|
40
|
-
[unreleased]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.
|
|
78
|
+
[unreleased]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.2.0...HEAD
|
|
79
|
+
[0.2.0]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.1.1...v0.2.0
|
|
80
|
+
[0.1.1]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.1.0...v0.1.1
|
|
41
81
|
[0.1.0]: https://github.com/galikvalkin/react-native-unified-action-sheet/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -12,8 +12,9 @@ Unified action sheet API for React Native: a native `UIAlertController` on iOS,
|
|
|
12
12
|
<em>iPad: the same sheet presented as a popover, anchored to the view that opened it.</em>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
-
- **Fully native, always on top.** The sheet gets its own platform window, so it renders above your
|
|
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
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.
|
|
17
18
|
- Works on React Native **0.81 (old and new architecture)** through the **latest** release.
|
|
18
19
|
- **No extra native dependencies** on either platform, and zero runtime dependencies (`react` / `react-native` peers only).
|
|
19
20
|
|
|
@@ -44,7 +45,7 @@ await showActionSheetWithOptions({
|
|
|
44
45
|
});
|
|
45
46
|
```
|
|
46
47
|
|
|
47
|
-
|
|
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:
|
|
48
49
|
|
|
49
50
|
| | promise resolves with | `onPress` runs |
|
|
50
51
|
| --- | --- | --- |
|
|
@@ -75,6 +76,34 @@ const anchorRef = useRef<View>(null);
|
|
|
75
76
|
|
|
76
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.
|
|
77
78
|
|
|
79
|
+
### Prompts
|
|
80
|
+
|
|
81
|
+
`showPromptWithOptions()` is the same dialog with a text field:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { showPromptWithOptions } from 'react-native-unified-action-sheet';
|
|
85
|
+
|
|
86
|
+
const result = await showPromptWithOptions({
|
|
87
|
+
title: 'Rename item',
|
|
88
|
+
placeholder: 'New name',
|
|
89
|
+
defaultValue: 'Untitled',
|
|
90
|
+
options: [
|
|
91
|
+
{ label: 'Save', onPress: (text) => rename(text) },
|
|
92
|
+
{ label: 'Cancel', style: 'cancel' },
|
|
93
|
+
],
|
|
94
|
+
});
|
|
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
|
+
|
|
78
107
|
### Dismissing from code
|
|
79
108
|
|
|
80
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`.
|
|
@@ -91,13 +120,13 @@ import {
|
|
|
91
120
|
| Option | Type | iOS | Android | Description |
|
|
92
121
|
| --- | --- | :---: | :---: | --- |
|
|
93
122
|
| `options` | `ActionSheetButtonInterface[]` | ✅ | ✅ | The buttons, in order: `{ label, style?, disabled?, onPress? }`. |
|
|
94
|
-
| `style` (per button) | `'cancel' \| 'destructive'?` | ✅ | ✅ | `'cancel'` renders a separated
|
|
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. |
|
|
95
124
|
| `disabled` (per button) | `boolean?` | ✅ | ✅ | Renders the row dimmed and unresponsive to taps. |
|
|
96
125
|
| `onPress` (per button) | `() => void?` | ✅ | ✅ | Runs when this button resolves the sheet. |
|
|
97
126
|
| `title` | `string?` | ✅ | ✅ | Sheet title. |
|
|
98
127
|
| `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
|
|
100
|
-
| `anchor` | `ActionSheetAnchorInterface?` | ✅ | ✅ |
|
|
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. |
|
|
101
130
|
| `anchorAlignment` | `'start' \| 'center'` | — | ✅ | Alignment of an `'anchored'` popup relative to its anchor. `'start'` (default) aligns leading edges, flipping in RTL. |
|
|
102
131
|
| `userInterfaceStyle` | `'light' \| 'dark'` | ✅ | ✅ | Forces the appearance; defaults to following the system setting. |
|
|
103
132
|
| `tintColor` | `string?` | ✅ | ✅ | Text color of non-destructive buttons. |
|
|
@@ -105,26 +134,16 @@ import {
|
|
|
105
134
|
| `destructiveColor` | `string?` | ✅ | ✅ | Overrides the destructive row color (Android's palette error color, iOS system red). |
|
|
106
135
|
| `buttonTextAlignment` | `'start' \| 'center'` | — | ✅ | Alignment of button labels. Defaults to `'start'`, which follows layout direction. |
|
|
107
136
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
import type {
|
|
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
|
-
```
|
|
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.
|
|
119
140
|
|
|
120
141
|
### Platform notes
|
|
121
142
|
|
|
122
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.
|
|
123
144
|
- **On iPad, a popover hides the cancel row**, since tapping outside already cancels. The index you receive is unaffected.
|
|
124
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.
|
|
125
|
-
- **Light or dark follows the system setting** unless `userInterfaceStyle` forces one, chosen when the sheet opens.
|
|
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.
|
|
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.
|
|
128
147
|
|
|
129
148
|
## Testing
|
|
130
149
|
|
|
@@ -136,7 +155,7 @@ jest.mock('react-native-unified-action-sheet', () =>
|
|
|
136
155
|
);
|
|
137
156
|
```
|
|
138
157
|
|
|
139
|
-
Every sheet then resolves with no selection. To simulate a tap, queue the index
|
|
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:
|
|
140
159
|
|
|
141
160
|
```ts
|
|
142
161
|
import { setNextButtonIndex } from 'react-native-unified-action-sheet/jest';
|
|
@@ -145,7 +164,9 @@ setNextButtonIndex(0);
|
|
|
145
164
|
await openTheSheet();
|
|
146
165
|
```
|
|
147
166
|
|
|
148
|
-
|
|
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.
|
|
149
170
|
|
|
150
171
|
## Compatibility
|
|
151
172
|
|
|
@@ -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,
|
|
@@ -2,6 +2,7 @@ package com.unifiedactionsheet
|
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
4
|
import android.app.Dialog
|
|
5
|
+
import com.facebook.react.bridge.Arguments
|
|
5
6
|
import com.facebook.react.bridge.LifecycleEventListener
|
|
6
7
|
import com.facebook.react.bridge.Promise
|
|
7
8
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
@@ -37,6 +38,19 @@ class UnifiedActionSheetModule(reactContext: ReactApplicationContext) :
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
override fun showPromptWithOptions(options: ReadableMap, promise: Promise) {
|
|
42
|
+
val parsed = PromptOptions.fromReadableMap(options)
|
|
43
|
+
val activity = reactApplicationContext.currentActivity
|
|
44
|
+
?: return promise.reject(
|
|
45
|
+
"E_NO_ACTIVITY",
|
|
46
|
+
"No current activity to attach the prompt to.",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
UiThreadUtil.runOnUiThread {
|
|
50
|
+
presentPrompt(activity, parsed, promise)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
override fun dismissActionSheet() {
|
|
41
55
|
UiThreadUtil.runOnUiThread {
|
|
42
56
|
openDialogs.lastOrNull()?.let { dialog ->
|
|
@@ -118,6 +132,47 @@ class UnifiedActionSheetModule(reactContext: ReactApplicationContext) :
|
|
|
118
132
|
dialog.show()
|
|
119
133
|
}
|
|
120
134
|
|
|
135
|
+
/// Mirrors presentSheet exactly, except the resolved value carries the text
|
|
136
|
+
/// as well as the index. The dialog joins the same openDialogs registry, so
|
|
137
|
+
/// dismissActionSheet()/dismissAllActionSheets() close prompts too.
|
|
138
|
+
private fun presentPrompt(
|
|
139
|
+
activity: Activity,
|
|
140
|
+
options: PromptOptions,
|
|
141
|
+
promise: Promise,
|
|
142
|
+
) {
|
|
143
|
+
val resolved = AtomicBoolean(false)
|
|
144
|
+
val resolveOnce: (Int?, String) -> Unit = { index, text ->
|
|
145
|
+
if (resolved.compareAndSet(false, true)) {
|
|
146
|
+
promise.resolve(
|
|
147
|
+
Arguments.createMap().apply {
|
|
148
|
+
putInt("buttonIndex", index ?: -1)
|
|
149
|
+
putString("text", text)
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
val (dialog, currentText) = buildPromptDialog(activity, options) { presented, index, text ->
|
|
156
|
+
presented.dismiss()
|
|
157
|
+
resolveOnce(index, text)
|
|
158
|
+
}
|
|
159
|
+
openDialogs.add(dialog)
|
|
160
|
+
|
|
161
|
+
// Read the field at dismissal time: what the user typed survives a backdrop
|
|
162
|
+
// tap, so a caller can still recover a draft.
|
|
163
|
+
dialog.setOnCancelListener { resolveOnce(options.cancelButtonIndex, currentText()) }
|
|
164
|
+
dialog.setOnDismissListener {
|
|
165
|
+
openDialogs.remove(dialog)
|
|
166
|
+
if (dismissedByApi.remove(dialog)) {
|
|
167
|
+
resolveOnce(DISMISSED_BY_API, currentText())
|
|
168
|
+
} else {
|
|
169
|
+
resolveOnce(options.cancelButtonIndex, currentText())
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
dialog.show()
|
|
174
|
+
}
|
|
175
|
+
|
|
121
176
|
companion object {
|
|
122
177
|
const val NAME = NativeUnifiedActionSheetSpec.NAME
|
|
123
178
|
|
|
@@ -73,6 +73,53 @@ RCT_EXPORT_METHOD(showActionSheetWithOptions
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
RCT_EXPORT_METHOD(showPromptWithOptions
|
|
77
|
+
: (JS::NativeUnifiedActionSheet::SpecShowPromptWithOptionsOptions &)options resolve
|
|
78
|
+
: (RCTPromiseResolveBlock)resolve reject
|
|
79
|
+
: (RCTPromiseRejectBlock)reject)
|
|
80
|
+
{
|
|
81
|
+
NSMutableDictionary *payload = [NSMutableDictionary new];
|
|
82
|
+
|
|
83
|
+
payload[@"options"] = RCTConvertVecToArray(options.options(), ^id(NSString *element) {
|
|
84
|
+
return element;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (options.cancelButtonIndex()) {
|
|
88
|
+
payload[@"cancelButtonIndex"] = @(*options.cancelButtonIndex());
|
|
89
|
+
}
|
|
90
|
+
if (options.destructiveButtonIndices()) {
|
|
91
|
+
payload[@"destructiveButtonIndices"] =
|
|
92
|
+
RCTConvertVecToArray(*options.destructiveButtonIndices(), ^id(double element) {
|
|
93
|
+
return @(element);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (options.disabledButtonIndices()) {
|
|
97
|
+
payload[@"disabledButtonIndices"] = RCTConvertVecToArray(*options.disabledButtonIndices(), ^id(double element) {
|
|
98
|
+
return @(element);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (options.secureTextEntry()) {
|
|
102
|
+
payload[@"secureTextEntry"] = @(*options.secureTextEntry());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
payload[@"title"] = options.title();
|
|
106
|
+
payload[@"message"] = options.message();
|
|
107
|
+
payload[@"placeholder"] = options.placeholder();
|
|
108
|
+
payload[@"defaultValue"] = options.defaultValue();
|
|
109
|
+
payload[@"keyboardType"] = options.keyboardType();
|
|
110
|
+
payload[@"tintColor"] = options.tintColor();
|
|
111
|
+
payload[@"cancelButtonTintColor"] = options.cancelButtonTintColor();
|
|
112
|
+
payload[@"destructiveColor"] = options.destructiveColor();
|
|
113
|
+
payload[@"userInterfaceStyle"] = options.userInterfaceStyle();
|
|
114
|
+
|
|
115
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
116
|
+
[UnifiedActionSheetImpl.shared showPromptWithOptions:payload
|
|
117
|
+
completion:^(NSInteger buttonIndex, NSString *text) {
|
|
118
|
+
resolve(@{@"buttonIndex" : @(buttonIndex), @"text" : text});
|
|
119
|
+
}];
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
76
123
|
RCT_EXPORT_METHOD(dismissActionSheet)
|
|
77
124
|
{
|
|
78
125
|
dispatch_async(dispatch_get_main_queue(), ^{
|