Skip to content

Commit 000bbe8

Browse files
robbie-cfacebook-github-bot
authored andcommitted
Add support for "preferred" AlertButton (#32538)
Summary: Currently, with the Alert API on iOS, the only way to bold one of the buttons is by setting the style to "cancel". This has the side-effect of moving it to the left. The underlying UIKit API has a way of setting a "preferred" button, which does not have this negative side-effect, so this PR wires this up. See preferredAction on UIAlertController https://developer.apple.com/documentation/uikit/uialertcontroller/ Docs PR: facebook/react-native-website#2839 ## Changelog [iOS] [Added] - Support setting an Alert button as "preferred", to emphasize it without needing to set it as a "cancel" button. Pull Request resolved: #32538 Test Plan: I ran the RNTesterPods app and added an example. It has a button styled with "preferred" and another with "cancel", to demonstrate that the "preferred" button takes emphasis over the "cancel" button. ![Simulator Screen Shot - iPhone 11 - 2021-11-04 at 09 48 35](https://user-images.githubusercontent.com/2056078/140292801-df880c43-c330-40df-b8e4-c1476c1645d6.png) Luna: * Also tested this on Catalyst {F754959632} Reviewed By: sammy-SC Differential Revision: D34357811 Pulled By: lunaleaps fbshipit-source-id: 3d860702c49cb219f950904ae0b9fabef03b5588
1 parent ee4ce2d commit 000bbe8

File tree

6 files changed

+103
-41
lines changed

6 files changed

+103
-41
lines changed

Libraries/Alert/Alert.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type AlertButtonStyle = 'default' | 'cancel' | 'destructive';
2222
export type Buttons = Array<{
2323
text?: string,
2424
onPress?: ?Function,
25+
isPreferred?: boolean,
2526
style?: AlertButtonStyle,
2627
...
2728
}>;
@@ -126,6 +127,7 @@ class Alert {
126127
const buttons = [];
127128
let cancelButtonKey;
128129
let destructiveButtonKey;
130+
let preferredButtonKey;
129131
if (typeof callbackOrButtons === 'function') {
130132
callbacks = [callbackOrButtons];
131133
} else if (Array.isArray(callbackOrButtons)) {
@@ -135,6 +137,8 @@ class Alert {
135137
cancelButtonKey = String(index);
136138
} else if (btn.style === 'destructive') {
137139
destructiveButtonKey = String(index);
140+
} else if (btn.isPreferred) {
141+
preferredButtonKey = String(index);
138142
}
139143
if (btn.text || index < (callbackOrButtons || []).length - 1) {
140144
const btnDef: {[number]: string} = {};
@@ -153,6 +157,7 @@ class Alert {
153157
defaultValue,
154158
cancelButtonKey,
155159
destructiveButtonKey,
160+
preferredButtonKey,
156161
keyboardType,
157162
userInterfaceStyle: options?.userInterfaceStyle || undefined,
158163
},

Libraries/Alert/NativeAlertManager.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type Args = {|
1919
defaultValue?: string,
2020
cancelButtonKey?: string,
2121
destructiveButtonKey?: string,
22+
preferredButtonKey?: string,
2223
keyboardType?: string,
2324
userInterfaceStyle?: string,
2425
|};

React/CoreModules/RCTAlertManager.mm

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ - (void)invalidate
9494
NSString *defaultValue = [RCTConvert NSString:args.defaultValue()];
9595
NSString *cancelButtonKey = [RCTConvert NSString:args.cancelButtonKey()];
9696
NSString *destructiveButtonKey = [RCTConvert NSString:args.destructiveButtonKey()];
97+
NSString *preferredButtonKey = [RCTConvert NSString:args.preferredButtonKey()];
9798
UIKeyboardType keyboardType = [RCTConvert UIKeyboardType:args.keyboardType()];
9899

99100
if (!title && !message) {
@@ -175,32 +176,37 @@ - (void)invalidate
175176
buttonStyle = UIAlertActionStyleDestructive;
176177
}
177178
__weak RCTAlertController *weakAlertController = alertController;
178-
[alertController
179-
addAction:[UIAlertAction
180-
actionWithTitle:buttonTitle
181-
style:buttonStyle
182-
handler:^(__unused UIAlertAction *action) {
183-
switch (type) {
184-
case RCTAlertViewStylePlainTextInput:
185-
case RCTAlertViewStyleSecureTextInput:
186-
callback(@[ buttonKey, [weakAlertController.textFields.firstObject text] ]);
187-
[weakAlertController hide];
188-
break;
189-
case RCTAlertViewStyleLoginAndPasswordInput: {
190-
NSDictionary<NSString *, NSString *> *loginCredentials = @{
191-
@"login" : [weakAlertController.textFields.firstObject text],
192-
@"password" : [weakAlertController.textFields.lastObject text]
193-
};
194-
callback(@[ buttonKey, loginCredentials ]);
195-
[weakAlertController hide];
196-
break;
197-
}
198-
case RCTAlertViewStyleDefault:
199-
callback(@[ buttonKey ]);
200-
[weakAlertController hide];
201-
break;
202-
}
203-
}]];
179+
180+
UIAlertAction *alertAction =
181+
[UIAlertAction actionWithTitle:buttonTitle
182+
style:buttonStyle
183+
handler:^(__unused UIAlertAction *action) {
184+
switch (type) {
185+
case RCTAlertViewStylePlainTextInput:
186+
case RCTAlertViewStyleSecureTextInput:
187+
callback(@[ buttonKey, [weakAlertController.textFields.firstObject text] ]);
188+
[weakAlertController hide];
189+
break;
190+
case RCTAlertViewStyleLoginAndPasswordInput: {
191+
NSDictionary<NSString *, NSString *> *loginCredentials = @{
192+
@"login" : [weakAlertController.textFields.firstObject text],
193+
@"password" : [weakAlertController.textFields.lastObject text]
194+
};
195+
callback(@[ buttonKey, loginCredentials ]);
196+
[weakAlertController hide];
197+
break;
198+
}
199+
case RCTAlertViewStyleDefault:
200+
callback(@[ buttonKey ]);
201+
[weakAlertController hide];
202+
break;
203+
}
204+
}];
205+
[alertController addAction:alertAction];
206+
207+
if ([buttonKey isEqualToString:preferredButtonKey]) {
208+
[alertController setPreferredAction:alertAction];
209+
}
204210
}
205211

206212
if (!_alertControllers) {

packages/rn-tester/js/examples/Alert/AlertExample.js

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
* LICENSE file in the root directory of this source tree.
66
*
77
* @format
8+
* @flow
89
*/
910

10-
import React, {useState} from 'react';
11+
import * as React from 'react';
12+
import type {RNTesterModule} from '../../types/RNTesterTypes';
1113
import {Alert, StyleSheet, Text, TouchableHighlight, View} from 'react-native';
1214

1315
// Shows log on the screen
14-
const Log = ({message}) =>
16+
const Log = ({message}: {message: string}) =>
1517
message ? (
1618
<View style={styles.logContainer}>
1719
<Text>
@@ -42,7 +44,7 @@ const AlertWithDefaultButton = () => {
4244
};
4345

4446
const AlertWithTwoButtons = () => {
45-
const [message, setMessage] = useState('');
47+
const [message, setMessage] = React.useState('');
4648

4749
const alertMessage = 'Your subscription has expired!';
4850

@@ -66,7 +68,7 @@ const AlertWithTwoButtons = () => {
6668
};
6769

6870
const AlertWithThreeButtons = () => {
69-
const [message, setMessage] = useState('');
71+
const [message, setMessage] = React.useState('');
7072

7173
const alertMessage = 'Do you want to save your changes?';
7274

@@ -92,7 +94,7 @@ const AlertWithThreeButtons = () => {
9294
};
9395

9496
const AlertWithManyButtons = () => {
95-
const [message, setMessage] = useState('');
97+
const [message, setMessage] = React.useState('');
9698

9799
const alertMessage =
98100
'Credibly reintermediate next-generation potentialities after goal-oriented ' +
@@ -122,7 +124,7 @@ const AlertWithManyButtons = () => {
122124
};
123125

124126
const AlertWithCancelableTrue = () => {
125-
const [message, setMessage] = useState('');
127+
const [message, setMessage] = React.useState('');
126128

127129
const alertMessage = 'Tapping outside this dialog will dismiss this alert.';
128130

@@ -154,7 +156,7 @@ const AlertWithCancelableTrue = () => {
154156
};
155157

156158
const AlertWithStyles = () => {
157-
const [message, setMessage] = useState('');
159+
const [message, setMessage] = React.useState('');
158160

159161
const alertMessage = 'Look at the button styles!';
160162

@@ -190,6 +192,39 @@ const AlertWithStyles = () => {
190192
);
191193
};
192194

195+
const AlertWithStylesPreferred = () => {
196+
const [message, setMessage] = React.useState('');
197+
198+
const alertMessage =
199+
"The Preferred button is styled with 'preferred', so it is emphasized over the cancel button.";
200+
201+
return (
202+
<View>
203+
<TouchableHighlight
204+
style={styles.wrapper}
205+
onPress={() =>
206+
Alert.alert('Foo Title', alertMessage, [
207+
{
208+
text: 'Preferred',
209+
isPreferred: true,
210+
onPress: () => setMessage('Preferred Pressed!'),
211+
},
212+
{
213+
text: 'Cancel',
214+
style: 'cancel',
215+
onPress: () => setMessage('Cancel Pressed!'),
216+
},
217+
])
218+
}>
219+
<View style={styles.button}>
220+
<Text>Tap to view alert</Text>
221+
</View>
222+
</TouchableHighlight>
223+
<Log message={message} />
224+
</View>
225+
);
226+
};
227+
193228
const styles = StyleSheet.create({
194229
wrapper: {
195230
borderRadius: 5,
@@ -208,12 +243,7 @@ const styles = StyleSheet.create({
208243
},
209244
});
210245

211-
exports.title = 'Alerts';
212-
exports.description =
213-
'Alerts display a concise and informative message ' +
214-
'and prompt the user to make a decision.';
215-
exports.documentationURL = 'https://reactnative.dev/docs/alert';
216-
exports.examples = [
246+
export const examples = [
217247
{
218248
title: 'Alert with default Button',
219249
description:
@@ -262,4 +292,24 @@ exports.examples = [
262292
return <AlertWithStyles />;
263293
},
264294
},
295+
{
296+
title: 'Alert with styles + preferred',
297+
platform: 'ios',
298+
description:
299+
"Alert buttons with 'isPreferred' will be emphasized, even over cancel buttons",
300+
render(): React.Node {
301+
return <AlertWithStylesPreferred />;
302+
},
303+
},
265304
];
305+
306+
export default ({
307+
framework: 'React',
308+
title: 'Alerts',
309+
category: 'UI',
310+
documentationURL: 'https://reactnative.dev/docs/alert',
311+
description:
312+
'Alerts display a concise and informative messageand prompt the user to make a decision.',
313+
showIndividualExamples: true,
314+
examples,
315+
}: RNTesterModule);

packages/rn-tester/js/examples/Alert/AlertIOSExample.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const {
1919
Alert,
2020
} = require('react-native');
2121

22-
const {examples: SharedAlertExamples} = require('./AlertExample');
22+
import {examples as SharedAlertExamples} from './AlertExample';
2323

2424
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
2525

packages/rn-tester/js/utils/RNTesterList.android.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ const APIs: Array<RNTesterModuleInfo> = [
142142
{
143143
key: 'AlertExample',
144144
category: 'UI',
145-
module: require('../examples/Alert/AlertExample'),
145+
module: require('../examples/Alert/AlertExample').default,
146146
},
147147
{
148148
key: 'AnimatedIndex',

0 commit comments

Comments
 (0)