Skip to content

Commit

Permalink
Add support for "preferred" AlertButton (#32538)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
robbie-c authored and facebook-github-bot committed Jul 22, 2022
1 parent ee4ce2d commit 000bbe8
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 41 deletions.
5 changes: 5 additions & 0 deletions Libraries/Alert/Alert.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type AlertButtonStyle = 'default' | 'cancel' | 'destructive';
export type Buttons = Array<{
text?: string,
onPress?: ?Function,
isPreferred?: boolean,
style?: AlertButtonStyle,
...
}>;
Expand Down Expand Up @@ -126,6 +127,7 @@ class Alert {
const buttons = [];
let cancelButtonKey;
let destructiveButtonKey;
let preferredButtonKey;
if (typeof callbackOrButtons === 'function') {
callbacks = [callbackOrButtons];
} else if (Array.isArray(callbackOrButtons)) {
Expand All @@ -135,6 +137,8 @@ class Alert {
cancelButtonKey = String(index);
} else if (btn.style === 'destructive') {
destructiveButtonKey = String(index);
} else if (btn.isPreferred) {
preferredButtonKey = String(index);
}
if (btn.text || index < (callbackOrButtons || []).length - 1) {
const btnDef: {[number]: string} = {};
Expand All @@ -153,6 +157,7 @@ class Alert {
defaultValue,
cancelButtonKey,
destructiveButtonKey,
preferredButtonKey,
keyboardType,
userInterfaceStyle: options?.userInterfaceStyle || undefined,
},
Expand Down
1 change: 1 addition & 0 deletions Libraries/Alert/NativeAlertManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type Args = {|
defaultValue?: string,
cancelButtonKey?: string,
destructiveButtonKey?: string,
preferredButtonKey?: string,
keyboardType?: string,
userInterfaceStyle?: string,
|};
Expand Down
58 changes: 32 additions & 26 deletions React/CoreModules/RCTAlertManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ - (void)invalidate
NSString *defaultValue = [RCTConvert NSString:args.defaultValue()];
NSString *cancelButtonKey = [RCTConvert NSString:args.cancelButtonKey()];
NSString *destructiveButtonKey = [RCTConvert NSString:args.destructiveButtonKey()];
NSString *preferredButtonKey = [RCTConvert NSString:args.preferredButtonKey()];
UIKeyboardType keyboardType = [RCTConvert UIKeyboardType:args.keyboardType()];

if (!title && !message) {
Expand Down Expand Up @@ -175,32 +176,37 @@ - (void)invalidate
buttonStyle = UIAlertActionStyleDestructive;
}
__weak RCTAlertController *weakAlertController = alertController;
[alertController
addAction:[UIAlertAction
actionWithTitle:buttonTitle
style:buttonStyle
handler:^(__unused UIAlertAction *action) {
switch (type) {
case RCTAlertViewStylePlainTextInput:
case RCTAlertViewStyleSecureTextInput:
callback(@[ buttonKey, [weakAlertController.textFields.firstObject text] ]);
[weakAlertController hide];
break;
case RCTAlertViewStyleLoginAndPasswordInput: {
NSDictionary<NSString *, NSString *> *loginCredentials = @{
@"login" : [weakAlertController.textFields.firstObject text],
@"password" : [weakAlertController.textFields.lastObject text]
};
callback(@[ buttonKey, loginCredentials ]);
[weakAlertController hide];
break;
}
case RCTAlertViewStyleDefault:
callback(@[ buttonKey ]);
[weakAlertController hide];
break;
}
}]];

UIAlertAction *alertAction =
[UIAlertAction actionWithTitle:buttonTitle
style:buttonStyle
handler:^(__unused UIAlertAction *action) {
switch (type) {
case RCTAlertViewStylePlainTextInput:
case RCTAlertViewStyleSecureTextInput:
callback(@[ buttonKey, [weakAlertController.textFields.firstObject text] ]);
[weakAlertController hide];
break;
case RCTAlertViewStyleLoginAndPasswordInput: {
NSDictionary<NSString *, NSString *> *loginCredentials = @{
@"login" : [weakAlertController.textFields.firstObject text],
@"password" : [weakAlertController.textFields.lastObject text]
};
callback(@[ buttonKey, loginCredentials ]);
[weakAlertController hide];
break;
}
case RCTAlertViewStyleDefault:
callback(@[ buttonKey ]);
[weakAlertController hide];
break;
}
}];
[alertController addAction:alertAction];

if ([buttonKey isEqualToString:preferredButtonKey]) {
[alertController setPreferredAction:alertAction];
}
}

if (!_alertControllers) {
Expand Down
76 changes: 63 additions & 13 deletions packages/rn-tester/js/examples/Alert/AlertExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/

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

// Shows log on the screen
const Log = ({message}) =>
const Log = ({message}: {message: string}) =>
message ? (
<View style={styles.logContainer}>
<Text>
Expand Down Expand Up @@ -42,7 +44,7 @@ const AlertWithDefaultButton = () => {
};

const AlertWithTwoButtons = () => {
const [message, setMessage] = useState('');
const [message, setMessage] = React.useState('');

const alertMessage = 'Your subscription has expired!';

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

const AlertWithThreeButtons = () => {
const [message, setMessage] = useState('');
const [message, setMessage] = React.useState('');

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

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

const AlertWithManyButtons = () => {
const [message, setMessage] = useState('');
const [message, setMessage] = React.useState('');

const alertMessage =
'Credibly reintermediate next-generation potentialities after goal-oriented ' +
Expand Down Expand Up @@ -122,7 +124,7 @@ const AlertWithManyButtons = () => {
};

const AlertWithCancelableTrue = () => {
const [message, setMessage] = useState('');
const [message, setMessage] = React.useState('');

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

Expand Down Expand Up @@ -154,7 +156,7 @@ const AlertWithCancelableTrue = () => {
};

const AlertWithStyles = () => {
const [message, setMessage] = useState('');
const [message, setMessage] = React.useState('');

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

Expand Down Expand Up @@ -190,6 +192,39 @@ const AlertWithStyles = () => {
);
};

const AlertWithStylesPreferred = () => {
const [message, setMessage] = React.useState('');

const alertMessage =
"The Preferred button is styled with 'preferred', so it is emphasized over the cancel button.";

return (
<View>
<TouchableHighlight
style={styles.wrapper}
onPress={() =>
Alert.alert('Foo Title', alertMessage, [
{
text: 'Preferred',
isPreferred: true,
onPress: () => setMessage('Preferred Pressed!'),
},
{
text: 'Cancel',
style: 'cancel',
onPress: () => setMessage('Cancel Pressed!'),
},
])
}>
<View style={styles.button}>
<Text>Tap to view alert</Text>
</View>
</TouchableHighlight>
<Log message={message} />
</View>
);
};

const styles = StyleSheet.create({
wrapper: {
borderRadius: 5,
Expand All @@ -208,12 +243,7 @@ const styles = StyleSheet.create({
},
});

exports.title = 'Alerts';
exports.description =
'Alerts display a concise and informative message ' +
'and prompt the user to make a decision.';
exports.documentationURL = 'https://reactnative.dev/docs/alert';
exports.examples = [
export const examples = [
{
title: 'Alert with default Button',
description:
Expand Down Expand Up @@ -262,4 +292,24 @@ exports.examples = [
return <AlertWithStyles />;
},
},
{
title: 'Alert with styles + preferred',
platform: 'ios',
description:
"Alert buttons with 'isPreferred' will be emphasized, even over cancel buttons",
render(): React.Node {
return <AlertWithStylesPreferred />;
},
},
];

export default ({
framework: 'React',
title: 'Alerts',
category: 'UI',
documentationURL: 'https://reactnative.dev/docs/alert',
description:
'Alerts display a concise and informative messageand prompt the user to make a decision.',
showIndividualExamples: true,
examples,
}: RNTesterModule);
2 changes: 1 addition & 1 deletion packages/rn-tester/js/examples/Alert/AlertIOSExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const {
Alert,
} = require('react-native');

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

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

Expand Down
2 changes: 1 addition & 1 deletion packages/rn-tester/js/utils/RNTesterList.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ const APIs: Array<RNTesterModuleInfo> = [
{
key: 'AlertExample',
category: 'UI',
module: require('../examples/Alert/AlertExample'),
module: require('../examples/Alert/AlertExample').default,
},
{
key: 'AnimatedIndex',
Expand Down

0 comments on commit 000bbe8

Please sign in to comment.