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/src/index.tsx CHANGED
@@ -1,12 +1,26 @@
1
1
  import { Platform } from 'react-native';
2
2
 
3
3
  import type { Spec } from './NativeUnifiedActionSheet';
4
- import type { ActionSheetOptionsInterface } from './action-sheet-options.interface';
4
+ import type {
5
+ ActionSheetOptionsInterface,
6
+ ActionSheetResultInterface,
7
+ } from './action-sheet-options.interface';
8
+ import type {
9
+ BaseButtonInterface,
10
+ CloseResultInterface,
11
+ } from './common-options.interface';
12
+ import type {
13
+ PromptButtonInterface,
14
+ PromptOptionsInterface,
15
+ PromptResultInterface,
16
+ } from './prompt-options.interface';
5
17
 
6
18
  export type {
7
19
  BaseButtonInterface,
8
20
  BaseOptionsInterface,
9
21
  BaseAndroidOptionsInterface,
22
+ CloseReason,
23
+ CloseResultInterface,
10
24
  } from './common-options.interface';
11
25
 
12
26
  export type {
@@ -15,11 +29,7 @@ export type {
15
29
  PromptAndroidOptionsInterface,
16
30
  PromptOptionsInterface,
17
31
  PromptResultInterface,
18
- } from './prompt-options.interface';
19
- import type { BaseButtonInterface } from './common-options.interface';
20
- import type {
21
- PromptOptionsInterface,
22
- PromptResultInterface,
32
+ PromptValuesInterface,
23
33
  } from './prompt-options.interface';
24
34
 
25
35
  export type {
@@ -27,21 +37,28 @@ export type {
27
37
  ActionSheetButtonInterface,
28
38
  ActionSheetCommonOptionsInterface,
29
39
  ActionSheetAndroidOptionsInterface,
40
+ ActionSheetDetent,
30
41
  ActionSheetOptionsInterface,
42
+ ActionSheetResultInterface,
31
43
  } from './action-sheet-options.interface';
32
44
 
33
45
  const DISMISSED_BY_API = -2;
34
46
 
35
47
  /// The native wire format is index-based; the public API is not. Nothing below
36
48
  /// index.tsx knows buttons are described as objects.
37
- type WireOptions = Omit<ActionSheetOptionsInterface, 'options' | 'anchor'> & {
49
+ type WireButtons = {
38
50
  options: string[];
39
51
  cancelButtonIndex?: number;
40
52
  destructiveButtonIndices?: number[];
41
53
  disabledButtonIndices?: number[];
42
- anchorRect?: { x: number; y: number; width: number; height: number };
54
+ preferredButtonIndex?: number;
43
55
  };
44
56
 
57
+ type WireOptions = Omit<ActionSheetOptionsInterface, 'options' | 'anchor'> &
58
+ WireButtons & {
59
+ anchorRect?: { x: number; y: number; width: number; height: number };
60
+ };
61
+
45
62
  const nativeModule = (): Spec =>
46
63
  require('./NativeUnifiedActionSheet').default as Spec;
47
64
 
@@ -58,20 +75,25 @@ const toWireOptions = ({
58
75
  ...toWireButtons(options),
59
76
  });
60
77
 
61
- type WirePromptOptions = Omit<PromptOptionsInterface, 'options'> & {
62
- options: string[];
63
- cancelButtonIndex?: number;
64
- destructiveButtonIndices?: number[];
65
- disabledButtonIndices?: number[];
66
- };
78
+ type WirePromptOptions = Omit<
79
+ PromptOptionsInterface,
80
+ 'options' | 'secureTextEntry'
81
+ > &
82
+ WireButtons & {
83
+ type: NonNullable<PromptOptionsInterface['type']>;
84
+ textRequiredButtonIndices?: number[];
85
+ };
67
86
 
68
87
  /// Same flattening as the sheet: labels plus index sets. Kept generic over the
69
88
  /// button shape so the two APIs cannot disagree about what 'cancel' means.
70
- const toWireButtons = (buttons: ReadonlyArray<BaseButtonInterface>) => {
89
+ const toWireButtons = (
90
+ buttons: ReadonlyArray<BaseButtonInterface>
91
+ ): WireButtons => {
71
92
  const labels: string[] = [];
72
93
  const destructive: number[] = [];
73
94
  const disabled: number[] = [];
74
95
  let cancelButtonIndex: number | undefined;
96
+ let preferredButtonIndex: number | undefined;
75
97
 
76
98
  buttons.forEach((button, index) => {
77
99
  labels.push(button.label);
@@ -81,6 +103,9 @@ const toWireButtons = (buttons: ReadonlyArray<BaseButtonInterface>) => {
81
103
  cancelButtonIndex = index;
82
104
  }
83
105
  if (button.disabled) disabled.push(index);
106
+ if (button.preferred && preferredButtonIndex == null) {
107
+ preferredButtonIndex = index;
108
+ }
84
109
  });
85
110
 
86
111
  return {
@@ -90,19 +115,68 @@ const toWireButtons = (buttons: ReadonlyArray<BaseButtonInterface>) => {
90
115
  ? {}
91
116
  : { destructiveButtonIndices: destructive }),
92
117
  ...(disabled.length === 0 ? {} : { disabledButtonIndices: disabled }),
118
+ ...(preferredButtonIndex == null ? {} : { preferredButtonIndex }),
93
119
  };
94
120
  };
95
121
 
122
+ /// Native sides only report an index: DISMISSED_BY_API, the cancel button's
123
+ /// index, -1 (a cancel gesture with no cancel button), or another button.
124
+ const toCloseResult = (
125
+ buttonIndex: number,
126
+ cancelButtonIndex: number | undefined
127
+ ): CloseResultInterface => {
128
+ if (buttonIndex === DISMISSED_BY_API) {
129
+ return { reason: 'dismissed', buttonIndex: undefined };
130
+ }
131
+ if (buttonIndex < 0 || buttonIndex === cancelButtonIndex) {
132
+ return { reason: 'cancelled', buttonIndex };
133
+ }
134
+
135
+ return { reason: 'selected', buttonIndex };
136
+ };
137
+
138
+ const DISMISSED: CloseResultInterface = {
139
+ reason: 'dismissed',
140
+ buttonIndex: undefined,
141
+ };
142
+
96
143
  const showWithNativeModule = (
97
144
  options: WireOptions
98
- ): Promise<number | undefined> =>
145
+ ): Promise<ActionSheetResultInterface> =>
99
146
  nativeModule()
100
147
  .showActionSheetWithOptions(options)
101
- // A programmatic dismiss resolves with no index rather than a selection.
148
+ // A native failure reads as a cancellation rather than a rejection.
149
+ .catch(() => options.cancelButtonIndex ?? -1)
102
150
  .then((buttonIndex) =>
103
- buttonIndex === DISMISSED_BY_API ? undefined : buttonIndex
104
- )
105
- .catch(() => options.cancelButtonIndex ?? -1);
151
+ toCloseResult(buttonIndex, options.cancelButtonIndex)
152
+ );
153
+
154
+ let warnedMaterialDisabled = false;
155
+
156
+ /// 'bottom' needs Material on Android, which the app opts into at build time.
157
+ /// Without it the sheet falls back to a centered dialog; say so once, in
158
+ /// development, since the fallback alone looks like the option was ignored.
159
+ const warnIfBottomUnavailable = (
160
+ presentationStyle: ActionSheetOptionsInterface['presentationStyle']
161
+ ) => {
162
+ if (
163
+ !__DEV__ ||
164
+ warnedMaterialDisabled ||
165
+ Platform.OS !== 'android' ||
166
+ presentationStyle !== 'bottom' ||
167
+ nativeModule().getConstants().isMaterialEnabled
168
+ ) {
169
+ return;
170
+ }
171
+
172
+ warnedMaterialDisabled = true;
173
+ console.warn(
174
+ "react-native-unified-action-sheet: presentationStyle 'bottom' needs Material on Android, " +
175
+ 'so this sheet falls back to a centered dialog. Set unifiedActionSheet.material=true in ' +
176
+ "android/gradle.properties, or add ['react-native-unified-action-sheet', { material: true }] " +
177
+ 'to your Expo plugins, then rebuild the app.'
178
+ );
179
+ };
106
180
 
107
181
  /// The anchor is measured in JS and sent across as a rect, so neither native
108
182
  /// module has to resolve a view: a ref's own measureInWindow is the supported
@@ -129,23 +203,30 @@ const withAnchorRect = (
129
203
 
130
204
  export const showActionSheetWithOptions = (
131
205
  options: ActionSheetOptionsInterface
132
- ): Promise<number | undefined> => {
206
+ ): Promise<ActionSheetResultInterface> => {
133
207
  const wire = toWireOptions(options);
134
208
 
135
- // -1 and undefined index nothing, so optional chaining covers both.
136
- const press = (buttonIndex: number | undefined) => {
137
- if (buttonIndex != null) options.options[buttonIndex]?.onPress?.();
209
+ // -1 and a dismissal index nothing, so optional chaining covers both.
210
+ const press = (result: ActionSheetResultInterface) => {
211
+ if (result.buttonIndex != null) {
212
+ options.options[result.buttonIndex]?.onPress?.();
213
+ }
138
214
 
139
- return buttonIndex;
215
+ return result;
140
216
  };
141
217
 
142
218
  if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
143
- return Promise.resolve(undefined);
219
+ return Promise.resolve(DISMISSED);
144
220
  }
145
221
 
146
- return withAnchorRect(wire, options.anchor)
147
- .then(showWithNativeModule)
148
- .then(press);
222
+ warnIfBottomUnavailable(options.presentationStyle);
223
+
224
+ // A bottom sheet has no anchor, so don't measure one: on iPad a rect would
225
+ // turn the sheet into an arrowed popover pointing at it.
226
+ const anchor =
227
+ options.presentationStyle === 'bottom' ? undefined : options.anchor;
228
+
229
+ return withAnchorRect(wire, anchor).then(showWithNativeModule).then(press);
149
230
  };
150
231
 
151
232
  export const dismissActionSheet = (): void => {
@@ -154,8 +235,8 @@ export const dismissActionSheet = (): void => {
154
235
  }
155
236
  };
156
237
 
157
- /// Closes every open sheet, not just the top-most one. Each resolves with no
158
- /// index, exactly as dismissActionSheet() does.
238
+ /// Closes every open sheet, not just the top-most one. Each resolves as
239
+ /// 'dismissed', exactly as with dismissActionSheet().
159
240
  export const dismissAllActionSheets = (): void => {
160
241
  if (Platform.OS === 'ios' || Platform.OS === 'android') {
161
242
  nativeModule().dismissAllActionSheets();
@@ -168,25 +249,49 @@ export const dismissAllActionSheets = (): void => {
168
249
  /// silent no-op on Android, which is the gap this fills.
169
250
  export const showPromptWithOptions = (
170
251
  options: PromptOptionsInterface
171
- ): Promise<PromptResultInterface | undefined> => {
172
- const { options: buttons, ...rest } = options;
173
- const wire: WirePromptOptions = { ...rest, ...toWireButtons(buttons) };
252
+ ): Promise<PromptResultInterface> => {
253
+ const { options: buttons, secureTextEntry, type, ...rest } = options;
254
+ const textRequired = buttons.flatMap(
255
+ (button: PromptButtonInterface, index) =>
256
+ button.requiresText ? [index] : []
257
+ );
258
+ const wire: WirePromptOptions = {
259
+ ...rest,
260
+ ...toWireButtons(buttons),
261
+ // One field of truth for the native sides: the old boolean maps onto type.
262
+ type: type ?? (secureTextEntry ? 'secure-text' : 'plain-text'),
263
+ ...(textRequired.length === 0
264
+ ? {}
265
+ : { textRequiredButtonIndices: textRequired }),
266
+ };
267
+ const hasPassword = wire.type === 'login-password';
174
268
 
175
269
  if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
176
- return Promise.resolve(undefined);
270
+ return Promise.resolve({ ...DISMISSED, text: '' });
177
271
  }
178
272
 
179
273
  return nativeModule()
180
274
  .showPromptWithOptions(wire)
181
- .then((result) =>
182
- // A programmatic dismiss resolves with no selection, matching the sheet.
183
- result.buttonIndex === DISMISSED_BY_API ? undefined : result
184
- )
185
- .catch(() => ({ buttonIndex: wire.cancelButtonIndex ?? -1, text: '' }))
275
+ .catch(() => ({
276
+ buttonIndex: wire.cancelButtonIndex ?? -1,
277
+ text: '',
278
+ password: '',
279
+ }))
280
+ .then(({ buttonIndex, text, password }): PromptResultInterface => ({
281
+ ...toCloseResult(buttonIndex, wire.cancelButtonIndex),
282
+ text,
283
+ // Even a dismissal carries the fields, so a draft is recoverable.
284
+ ...(hasPassword ? { password } : {}),
285
+ }))
186
286
  .then((result) => {
187
- // The text goes to the handler, so a caller using onPress alone never
287
+ // The values go to the handler, so a caller using onPress alone never
188
288
  // has to read the resolved value.
189
- if (result) buttons[result.buttonIndex]?.onPress?.(result.text);
289
+ if (result.buttonIndex != null) {
290
+ const { text, password } = result;
291
+ buttons[result.buttonIndex]?.onPress?.(
292
+ hasPassword ? { text, password } : { text }
293
+ );
294
+ }
190
295
 
191
296
  return result;
192
297
  });
@@ -2,17 +2,35 @@ import type {
2
2
  BaseAndroidOptionsInterface,
3
3
  BaseButtonInterface,
4
4
  BaseOptionsInterface,
5
+ CloseResultInterface,
5
6
  } from './common-options.interface';
6
7
 
8
+ /// What was in the field(s) when the prompt closed. password is present only
9
+ /// for type 'login-password'.
10
+ export interface PromptValuesInterface {
11
+ text: string;
12
+ password?: string;
13
+ }
14
+
7
15
  export interface PromptButtonInterface extends BaseButtonInterface {
8
- onPress?: (text: string) => void;
16
+ onPress?: (values: PromptValuesInterface) => void;
17
+ /// Keeps this button disabled while the field is empty (for
18
+ /// 'login-password', while either field is).
19
+ requiresText?: boolean;
9
20
  }
10
21
 
11
22
  export interface PromptCommonOptionsInterface extends BaseOptionsInterface {
12
23
  options: PromptButtonInterface[];
24
+ /// As in React Native's Alert.prompt. 'login-password' shows a second,
25
+ /// secure field below the first. Defaults to 'plain-text'.
26
+ type?: 'plain-text' | 'secure-text' | 'login-password';
13
27
  placeholder?: string;
28
+ /// Placeholder of the password field of a 'login-password' prompt.
29
+ passwordPlaceholder?: string;
14
30
  defaultValue?: string;
15
31
  keyboardType?: 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'url';
32
+ /// Same as type: 'secure-text'. Kept for compatibility; type wins when both
33
+ /// are set.
16
34
  secureTextEntry?: boolean;
17
35
  }
18
36
 
@@ -21,7 +39,5 @@ export type PromptAndroidOptionsInterface = BaseAndroidOptionsInterface;
21
39
  export interface PromptOptionsInterface
22
40
  extends PromptCommonOptionsInterface, PromptAndroidOptionsInterface {}
23
41
 
24
- export interface PromptResultInterface {
25
- buttonIndex: number;
26
- text: string;
27
- }
42
+ export type PromptResultInterface = CloseResultInterface &
43
+ PromptValuesInterface;