react-native-unified-action-sheet 0.3.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 (45) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/README.md +24 -4
  3. package/android/src/main/java/com/unifiedactionsheet/ActionSheetOptions.kt +35 -0
  4. package/android/src/main/java/com/unifiedactionsheet/PromptOptions.kt +27 -5
  5. package/android/src/main/java/com/unifiedactionsheet/SheetContentBuilder.kt +75 -9
  6. package/android/src/main/java/com/unifiedactionsheet/SheetPresenters.kt +61 -10
  7. package/android/src/main/java/com/unifiedactionsheet/UnifiedActionSheetModule.kt +8 -7
  8. package/android/src/material/java/com/unifiedactionsheet/MaterialBottomSheetPresenter.kt +65 -2
  9. package/ios/BottomSheetViewController.swift +3 -1
  10. package/ios/UnifiedActionSheet.mm +25 -4
  11. package/ios/UnifiedActionSheetImpl.swift +124 -27
  12. package/jest/index.d.ts +12 -9
  13. package/jest/index.js +37 -18
  14. package/lib/commonjs/NativeUnifiedActionSheet.js.map +1 -1
  15. package/lib/commonjs/index.js +84 -17
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/module/NativeUnifiedActionSheet.js.map +1 -1
  18. package/lib/module/index.js +84 -17
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts +7 -1
  21. package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts.map +1 -1
  22. package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts +4 -1
  23. package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts.map +1 -1
  24. package/lib/typescript/commonjs/src/common-options.interface.d.ts +9 -0
  25. package/lib/typescript/commonjs/src/common-options.interface.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/src/index.d.ts +6 -6
  27. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/src/prompt-options.interface.d.ts +10 -6
  29. package/lib/typescript/commonjs/src/prompt-options.interface.d.ts.map +1 -1
  30. package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts +7 -1
  31. package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts.map +1 -1
  32. package/lib/typescript/module/src/action-sheet-options.interface.d.ts +4 -1
  33. package/lib/typescript/module/src/action-sheet-options.interface.d.ts.map +1 -1
  34. package/lib/typescript/module/src/common-options.interface.d.ts +9 -0
  35. package/lib/typescript/module/src/common-options.interface.d.ts.map +1 -1
  36. package/lib/typescript/module/src/index.d.ts +6 -6
  37. package/lib/typescript/module/src/index.d.ts.map +1 -1
  38. package/lib/typescript/module/src/prompt-options.interface.d.ts +10 -6
  39. package/lib/typescript/module/src/prompt-options.interface.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/NativeUnifiedActionSheet.ts +7 -2
  42. package/src/action-sheet-options.interface.ts +11 -0
  43. package/src/common-options.interface.ts +18 -0
  44. package/src/index.tsx +112 -39
  45. package/src/prompt-options.interface.ts +21 -5
@@ -20,6 +20,8 @@ export interface Spec extends TurboModule {
20
20
  userInterfaceStyle?: string;
21
21
  presentationStyle?: string;
22
22
  anchorAlignment?: string;
23
+ preferredButtonIndex?: number;
24
+ detents?: string[];
23
25
  anchorRect?: {
24
26
  x: number;
25
27
  y: number;
@@ -34,16 +36,19 @@ export interface Spec extends TurboModule {
34
36
  disabledButtonIndices?: number[];
35
37
  title?: string;
36
38
  message?: string;
39
+ preferredButtonIndex?: number;
40
+ textRequiredButtonIndices?: number[];
41
+ type?: string;
37
42
  placeholder?: string;
43
+ passwordPlaceholder?: string;
38
44
  defaultValue?: string;
39
45
  keyboardType?: string;
40
- secureTextEntry?: boolean;
41
46
  tintColor?: string;
42
47
  cancelButtonTintColor?: string;
43
48
  destructiveColor?: string;
44
49
  buttonTextAlignment?: string;
45
50
  userInterfaceStyle?: string;
46
- }): Promise<{ buttonIndex: number; text: string }>;
51
+ }): Promise<{ buttonIndex: number; text: string; password: string }>;
47
52
  dismissActionSheet(): void;
48
53
  dismissAllActionSheets(): void;
49
54
  }
@@ -2,6 +2,7 @@ import type {
2
2
  BaseAndroidOptionsInterface,
3
3
  BaseButtonInterface,
4
4
  BaseOptionsInterface,
5
+ CloseResultInterface,
5
6
  } from './common-options.interface';
6
7
 
7
8
  export interface ActionSheetButtonInterface extends BaseButtonInterface {
@@ -14,6 +15,10 @@ export interface ActionSheetAnchorInterface {
14
15
  ) => void;
15
16
  }
16
17
 
18
+ /// A bottom sheet's height: 'auto' fits the content, 'medium' is half the
19
+ /// screen, 'large' is the full height below the status bar.
20
+ export type ActionSheetDetent = 'auto' | 'medium' | 'large';
21
+
17
22
  export interface ActionSheetCommonOptionsInterface extends BaseOptionsInterface {
18
23
  options: ActionSheetButtonInterface[];
19
24
  anchor?:
@@ -21,6 +26,10 @@ export interface ActionSheetCommonOptionsInterface extends BaseOptionsInterface
21
26
  | { current: ActionSheetAnchorInterface | null }
22
27
  | null;
23
28
  presentationStyle?: 'centered' | 'anchored' | 'bottom';
29
+ /// 'bottom' only: the heights the sheet can rest at. It opens at the first
30
+ /// and can be dragged to the others. Unset, a short list fits its content
31
+ /// and a long one opens at half height and expands to full.
32
+ detents?: ActionSheetDetent[];
24
33
  }
25
34
 
26
35
  export interface ActionSheetAndroidOptionsInterface extends BaseAndroidOptionsInterface {
@@ -31,3 +40,5 @@ export interface ActionSheetOptionsInterface
31
40
  extends
32
41
  ActionSheetCommonOptionsInterface,
33
42
  ActionSheetAndroidOptionsInterface {}
43
+
44
+ export type ActionSheetResultInterface = CloseResultInterface;
@@ -2,6 +2,11 @@ export interface BaseButtonInterface {
2
2
  label: string;
3
3
  style?: 'cancel' | 'destructive';
4
4
  disabled?: boolean;
5
+ /// The default action, emphasized: bold on Android and in the iOS bottom
6
+ /// sheet; in iOS alerts and prompts UIKit's own preferred-action style (a
7
+ /// filled button from iOS 26, bold text before). Only the first preferred
8
+ /// button counts. UIKit ignores it in the standard iOS action sheet.
9
+ preferred?: boolean;
5
10
  }
6
11
 
7
12
  export interface BaseOptionsInterface {
@@ -16,3 +21,16 @@ export interface BaseOptionsInterface {
16
21
  export interface BaseAndroidOptionsInterface {
17
22
  buttonTextAlignment?: 'start' | 'center';
18
23
  }
24
+
25
+ /// Why a sheet or prompt closed.
26
+ /// - 'selected': a button other than the cancel button.
27
+ /// - 'cancelled': the cancel button, a backdrop tap, back, or a swipe down.
28
+ /// - 'dismissed': dismissActionSheet(), dismissAllActionSheets(), or a JS
29
+ /// reload; also on a platform without the native module.
30
+ export type CloseReason = 'selected' | 'cancelled' | 'dismissed';
31
+
32
+ /// buttonIndex is the button that closed it. A cancellation carries the cancel
33
+ /// button's index, or -1 when there is none; a dismissal carries no index.
34
+ export type CloseResultInterface =
35
+ | { reason: 'selected' | 'cancelled'; buttonIndex: number }
36
+ | { reason: 'dismissed'; buttonIndex: undefined };
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,41 @@ 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
+ );
106
153
 
107
154
  let warnedMaterialDisabled = false;
108
155
 
@@ -156,18 +203,20 @@ const withAnchorRect = (
156
203
 
157
204
  export const showActionSheetWithOptions = (
158
205
  options: ActionSheetOptionsInterface
159
- ): Promise<number | undefined> => {
206
+ ): Promise<ActionSheetResultInterface> => {
160
207
  const wire = toWireOptions(options);
161
208
 
162
- // -1 and undefined index nothing, so optional chaining covers both.
163
- const press = (buttonIndex: number | undefined) => {
164
- 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
+ }
165
214
 
166
- return buttonIndex;
215
+ return result;
167
216
  };
168
217
 
169
218
  if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
170
- return Promise.resolve(undefined);
219
+ return Promise.resolve(DISMISSED);
171
220
  }
172
221
 
173
222
  warnIfBottomUnavailable(options.presentationStyle);
@@ -186,8 +235,8 @@ export const dismissActionSheet = (): void => {
186
235
  }
187
236
  };
188
237
 
189
- /// Closes every open sheet, not just the top-most one. Each resolves with no
190
- /// 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().
191
240
  export const dismissAllActionSheets = (): void => {
192
241
  if (Platform.OS === 'ios' || Platform.OS === 'android') {
193
242
  nativeModule().dismissAllActionSheets();
@@ -200,25 +249,49 @@ export const dismissAllActionSheets = (): void => {
200
249
  /// silent no-op on Android, which is the gap this fills.
201
250
  export const showPromptWithOptions = (
202
251
  options: PromptOptionsInterface
203
- ): Promise<PromptResultInterface | undefined> => {
204
- const { options: buttons, ...rest } = options;
205
- 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';
206
268
 
207
269
  if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
208
- return Promise.resolve(undefined);
270
+ return Promise.resolve({ ...DISMISSED, text: '' });
209
271
  }
210
272
 
211
273
  return nativeModule()
212
274
  .showPromptWithOptions(wire)
213
- .then((result) =>
214
- // A programmatic dismiss resolves with no selection, matching the sheet.
215
- result.buttonIndex === DISMISSED_BY_API ? undefined : result
216
- )
217
- .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
+ }))
218
286
  .then((result) => {
219
- // 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
220
288
  // has to read the resolved value.
221
- 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
+ }
222
295
 
223
296
  return result;
224
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;