Add secure and login-password types to AlertIOS.
Summary: Request from issue #3893 * Added support for `secure-text` and `login-password` types to AlertIOS. * Fixed and extended the cancel button highlighting functionality, which was broken at some point * Added localization for default `OK` and `Cancel` labels when using UIAlertController Closes https://github.com/facebook/react-native/pull/4401 Reviewed By: javache Differential Revision: D2702052 Pulled By: nicklockwood fb-gh-sync-id: cce312d7fec949f5fd2a7c656e65c657c4832c8f
This commit is contained in:
parent
7242efde0a
commit
f025049b6c
|
@ -43,7 +43,7 @@ exports.examples = [{
|
|||
</TouchableHighlight>
|
||||
<TouchableHighlight style={styles.wrapper}
|
||||
onPress={() => AlertIOS.alert(
|
||||
null,
|
||||
'Foo Title',
|
||||
null,
|
||||
[
|
||||
{text: 'Button', onPress: () => console.log('Button Pressed!')},
|
||||
|
@ -97,6 +97,87 @@ exports.examples = [{
|
|||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Alert Types',
|
||||
render() {
|
||||
return (
|
||||
<View>
|
||||
<TouchableHighlight
|
||||
style={styles.wrapper}
|
||||
onPress={() => AlertIOS.alert(
|
||||
'Hello World',
|
||||
null,
|
||||
[
|
||||
{text: 'OK', onPress: (text) => console.log('OK pressed')},
|
||||
],
|
||||
'default'
|
||||
)}>
|
||||
|
||||
<View style={styles.button}>
|
||||
<Text>
|
||||
{'default'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
</TouchableHighlight>
|
||||
<TouchableHighlight
|
||||
style={styles.wrapper}
|
||||
onPress={() => AlertIOS.alert(
|
||||
'Plain Text Entry',
|
||||
null,
|
||||
[
|
||||
{text: 'Submit', onPress: (text) => console.log('Text: ' + text)},
|
||||
],
|
||||
'plain-text'
|
||||
)}>
|
||||
|
||||
<View style={styles.button}>
|
||||
<Text>
|
||||
plain-text
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
</TouchableHighlight>
|
||||
<TouchableHighlight
|
||||
style={styles.wrapper}
|
||||
onPress={() => AlertIOS.alert(
|
||||
'Secure Text Entry',
|
||||
null,
|
||||
[
|
||||
{text: 'Submit', onPress: (text) => console.log('Password: ' + text)},
|
||||
],
|
||||
'secure-text'
|
||||
)}>
|
||||
|
||||
<View style={styles.button}>
|
||||
<Text>
|
||||
secure-text
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
</TouchableHighlight>
|
||||
<TouchableHighlight
|
||||
style={styles.wrapper}
|
||||
onPress={() => AlertIOS.alert(
|
||||
'Login & Password',
|
||||
null,
|
||||
[
|
||||
{text: 'Submit', onPress: (details) => console.log('Login: ' + details.login + '; Password: ' + details.password)},
|
||||
],
|
||||
'login-password'
|
||||
)}>
|
||||
|
||||
<View style={styles.button}>
|
||||
<Text>
|
||||
login-password
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Prompt',
|
||||
render(): React.Component {
|
||||
|
@ -116,10 +197,11 @@ class PromptExample extends React.Component {
|
|||
this.title = 'Type a value';
|
||||
this.defaultValue = 'Default value';
|
||||
this.buttons = [{
|
||||
text: 'Custom cancel',
|
||||
}, {
|
||||
text: 'Custom OK',
|
||||
onPress: this.promptResponse
|
||||
}, {
|
||||
text: 'Custom Cancel',
|
||||
style: 'cancel',
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
|
@ -14,11 +14,18 @@
|
|||
var RCTAlertManager = require('NativeModules').AlertManager;
|
||||
var invariant = require('invariant');
|
||||
|
||||
var DEFAULT_BUTTON_TEXT = 'OK';
|
||||
var DEFAULT_BUTTON = {
|
||||
text: DEFAULT_BUTTON_TEXT,
|
||||
onPress: null,
|
||||
};
|
||||
type AlertType = $Enum<{
|
||||
'default': string;
|
||||
'plain-text': string;
|
||||
'secure-text': string;
|
||||
'login-password': string;
|
||||
}>;
|
||||
|
||||
type AlertButtonStyle = $Enum<{
|
||||
'default': string;
|
||||
'cancel': string;
|
||||
'destructive': string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Launches an alert dialog with the specified title and message.
|
||||
|
@ -27,16 +34,13 @@ var DEFAULT_BUTTON = {
|
|||
* respective onPress callback and dismiss the alert. By default, the only
|
||||
* button will be an 'OK' button
|
||||
*
|
||||
* The last button in the list will be considered the 'Primary' button and
|
||||
* it will appear bold.
|
||||
*
|
||||
* ```
|
||||
* AlertIOS.alert(
|
||||
* 'Foo Title',
|
||||
* 'My Alert Msg',
|
||||
* [
|
||||
* {text: 'Foo', onPress: () => console.log('Foo Pressed!')},
|
||||
* {text: 'Bar', onPress: () => console.log('Bar Pressed!')},
|
||||
* {text: 'OK', onPress: () => console.log('OK Pressed!')},
|
||||
* {text: 'Cancel', onPress: () => console.log('Cancel Pressed!'), style: 'cancel'},
|
||||
* ]
|
||||
* )
|
||||
* ```
|
||||
|
@ -47,29 +51,36 @@ class AlertIOS {
|
|||
title: ?string,
|
||||
message?: ?string,
|
||||
buttons?: Array<{
|
||||
text: ?string;
|
||||
text?: string;
|
||||
onPress?: ?Function;
|
||||
style?: AlertButtonStyle;
|
||||
}>,
|
||||
type?: ?string
|
||||
type?: ?AlertType
|
||||
): void {
|
||||
var callbacks = [];
|
||||
var buttonsSpec = [];
|
||||
title = title || '';
|
||||
message = message || '';
|
||||
buttons = buttons || [DEFAULT_BUTTON];
|
||||
type = type || '';
|
||||
|
||||
buttons.forEach((btn, index) => {
|
||||
var cancelButtonKey;
|
||||
var destructiveButtonKey;
|
||||
buttons && buttons.forEach((btn, index) => {
|
||||
callbacks[index] = btn.onPress;
|
||||
if (btn.style == 'cancel') {
|
||||
cancelButtonKey = String(index);
|
||||
} else if (btn.style == 'destructive') {
|
||||
destructiveButtonKey = String(index);
|
||||
}
|
||||
if (btn.text || index < (buttons || []).length - 1) {
|
||||
var btnDef = {};
|
||||
btnDef[index] = btn.text || DEFAULT_BUTTON_TEXT;
|
||||
btnDef[index] = btn.text || '';
|
||||
buttonsSpec.push(btnDef);
|
||||
}
|
||||
});
|
||||
RCTAlertManager.alertWithArgs({
|
||||
title,
|
||||
message,
|
||||
title: title || undefined,
|
||||
message: message || undefined,
|
||||
buttons: buttonsSpec,
|
||||
type,
|
||||
type: type || undefined,
|
||||
cancelButtonKey,
|
||||
destructiveButtonKey,
|
||||
}, (id, value) => {
|
||||
var cb = callbacks[id];
|
||||
cb && cb(value);
|
||||
|
@ -80,8 +91,9 @@ class AlertIOS {
|
|||
title: string,
|
||||
value?: string,
|
||||
buttons?: Array<{
|
||||
text: ?string;
|
||||
text?: string;
|
||||
onPress?: ?Function;
|
||||
style?: AlertButtonStyle;
|
||||
}>,
|
||||
callback?: ?Function
|
||||
): void {
|
||||
|
@ -104,12 +116,7 @@ class AlertIOS {
|
|||
);
|
||||
|
||||
if (!buttons) {
|
||||
buttons = [{
|
||||
text: 'Cancel',
|
||||
}, {
|
||||
text: 'OK',
|
||||
onPress: callback
|
||||
}];
|
||||
buttons = [{ onPress: callback }];
|
||||
}
|
||||
this.alert(title, value, buttons, 'plain-text');
|
||||
}
|
||||
|
|
|
@ -102,3 +102,6 @@ RCT_EXTERN BOOL RCTIsXCAssetURL(NSURL *imageURL);
|
|||
|
||||
// Converts a CGColor to a hex string
|
||||
RCT_EXTERN NSString *RCTColorToHexString(CGColorRef color);
|
||||
|
||||
// Get standard localized string (if it exists)
|
||||
RCT_EXTERN NSString *RCTUIKitLocalizedString(NSString *string);
|
||||
|
|
|
@ -575,3 +575,11 @@ NSString *RCTColorToHexString(CGColorRef color)
|
|||
return [NSString stringWithFormat:@"#%02x%02x%02x", r, g, b];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// (https://github.com/0xced/XCDFormInputAccessoryView/blob/master/XCDFormInputAccessoryView/XCDFormInputAccessoryView.m#L10-L14)
|
||||
RCT_EXTERN NSString *RCTUIKitLocalizedString(NSString *string)
|
||||
{
|
||||
NSBundle *UIKitBundle = [NSBundle bundleForClass:[UIApplication class]];
|
||||
return UIKitBundle ? [UIKitBundle localizedStringForKey:string value:string table:nil] : string;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,17 @@
|
|||
#import "RCTLog.h"
|
||||
#import "RCTUtils.h"
|
||||
|
||||
@implementation RCTConvert (UIAlertViewStyle)
|
||||
|
||||
RCT_ENUM_CONVERTER(UIAlertViewStyle, (@{
|
||||
@"default": @(UIAlertViewStyleDefault),
|
||||
@"secure-text": @(UIAlertViewStyleSecureTextInput),
|
||||
@"plain-text": @(UIAlertViewStylePlainTextInput),
|
||||
@"login-password": @(UIAlertViewStyleLoginAndPasswordInput),
|
||||
}), UIAlertViewStyleDefault, integerValue)
|
||||
|
||||
@end
|
||||
|
||||
@interface RCTAlertManager() <UIAlertViewDelegate>
|
||||
|
||||
@end
|
||||
|
@ -50,58 +61,69 @@ RCT_EXPORT_MODULE()
|
|||
* @"message": @"<Alert message>",
|
||||
* @"buttons": @[
|
||||
* @{@"<key1>": @"<title1>"},
|
||||
* @{@"<key2>": @"<cancelButtonTitle>"},
|
||||
* ]
|
||||
* @{@"<key2>": @"<title2>"},
|
||||
* ],
|
||||
* @"cancelButtonKey": @"<key2>",
|
||||
* }
|
||||
* The key from the `buttons` dictionary is passed back in the callback on click.
|
||||
* Buttons are displayed in the order they are specified. If "cancel" is used as
|
||||
* the button key, it will be differently highlighted, according to iOS UI conventions.
|
||||
* Buttons are displayed in the order they are specified.
|
||||
*/
|
||||
RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args
|
||||
callback:(RCTResponseSenderBlock)callback)
|
||||
{
|
||||
NSString *title = [RCTConvert NSString:args[@"title"]];
|
||||
NSString *message = [RCTConvert NSString:args[@"message"]];
|
||||
NSString *type = [RCTConvert NSString:args[@"type"]];
|
||||
NSDictionaryArray *buttons = [RCTConvert NSDictionaryArray:args[@"buttons"]];
|
||||
BOOL allowsTextInput = [type isEqual:@"plain-text"];
|
||||
UIAlertViewStyle type = [RCTConvert UIAlertViewStyle:args[@"type"]];
|
||||
NSArray<NSDictionary *> *buttons = [RCTConvert NSDictionaryArray:args[@"buttons"]];
|
||||
NSString *cancelButtonKey = [RCTConvert NSString:args[@"cancelButtonKey"]];
|
||||
NSString *destructiveButtonKey = [RCTConvert NSString:args[@"destructiveButtonKey"]];
|
||||
|
||||
if (!title && !message) {
|
||||
RCTLogError(@"Must specify either an alert title, or message, or both");
|
||||
return;
|
||||
} else if (buttons.count == 0) {
|
||||
RCTLogError(@"Must have at least one button.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (buttons.count == 0) {
|
||||
if (type == UIAlertViewStyleDefault) {
|
||||
buttons = @[@{@"0": RCTUIKitLocalizedString(@"OK")}];
|
||||
cancelButtonKey = @"0";
|
||||
} else {
|
||||
buttons = @[
|
||||
@{@"0": RCTUIKitLocalizedString(@"OK")},
|
||||
@{@"1": RCTUIKitLocalizedString(@"Cancel")},
|
||||
];
|
||||
cancelButtonKey = @"1";
|
||||
}
|
||||
}
|
||||
|
||||
#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0
|
||||
|
||||
// TODO: we've encountered some bug when presenting alerts on top of a window that is subsequently
|
||||
// dismissed. As a temporary solution to this, we'll use UIAlertView preferentially if it's available.
|
||||
BOOL preferAlertView = (!RCTRunningInAppExtension() && UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone);
|
||||
// TODO: we've encountered some bug when presenting alerts on top of a window
|
||||
// that is subsequently dismissed. As a temporary solution to this, we'll use
|
||||
// UIAlertView preferentially if it's available and supports our use case.
|
||||
BOOL preferAlertView = (!RCTRunningInAppExtension() &&
|
||||
!destructiveButtonKey &&
|
||||
UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone);
|
||||
|
||||
if (preferAlertView || [UIAlertController class] == nil) {
|
||||
|
||||
UIAlertView *alertView = RCTAlertView(title, nil, self, nil, nil);
|
||||
NSMutableArray<NSString *> *buttonKeys = [[NSMutableArray alloc] initWithCapacity:buttons.count];
|
||||
|
||||
if (allowsTextInput) {
|
||||
alertView.alertViewStyle = UIAlertViewStylePlainTextInput;
|
||||
[alertView textFieldAtIndex:0].text = message;
|
||||
} else {
|
||||
alertView.alertViewStyle = type;
|
||||
alertView.message = message;
|
||||
}
|
||||
|
||||
NSMutableArray<NSString *> *buttonKeys =
|
||||
[[NSMutableArray alloc] initWithCapacity:buttons.count];
|
||||
|
||||
NSInteger index = 0;
|
||||
for (NSDictionary *button in buttons) {
|
||||
for (NSDictionary<NSString *, id> *button in buttons) {
|
||||
if (button.count != 1) {
|
||||
RCTLogError(@"Button definitions should have exactly one key.");
|
||||
}
|
||||
NSString *buttonKey = button.allKeys.firstObject;
|
||||
NSString *buttonTitle = [button[buttonKey] description];
|
||||
NSString *buttonTitle = [RCTConvert NSString:button[buttonKey]];
|
||||
[alertView addButtonWithTitle:buttonTitle];
|
||||
if ([buttonKey isEqualToString:@"cancel"]) {
|
||||
alertView.cancelButtonIndex = index;
|
||||
if ([buttonKey isEqualToString:cancelButtonKey]) {
|
||||
alertView.cancelButtonIndex = buttonKeys.count;
|
||||
}
|
||||
[buttonKeys addObject:buttonKey];
|
||||
index ++;
|
||||
|
@ -129,8 +151,9 @@ RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args
|
|||
return;
|
||||
}
|
||||
|
||||
// Walk the chain up to get the topmost modal view controller. If modals are presented,
|
||||
// the root view controller's view might not be in the window hierarchy, and presenting from it will fail.
|
||||
// Walk the chain up to get the topmost modal view controller. If modals are
|
||||
// presented the root view controller's view might not be in the window
|
||||
// hierarchy, and presenting from it will fail.
|
||||
while (presentingController.presentedViewController) {
|
||||
presentingController = presentingController.presentedViewController;
|
||||
}
|
||||
|
@ -139,32 +162,63 @@ RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args
|
|||
[UIAlertController alertControllerWithTitle:title
|
||||
message:nil
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
if (allowsTextInput) {
|
||||
switch (type) {
|
||||
case UIAlertViewStylePlainTextInput:
|
||||
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||||
textField.text = message;
|
||||
textField.secureTextEntry = NO;
|
||||
}];
|
||||
} else {
|
||||
alertController.message = message;
|
||||
break;
|
||||
case UIAlertViewStyleSecureTextInput:
|
||||
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||||
textField.placeholder = RCTUIKitLocalizedString(@"Password");
|
||||
textField.secureTextEntry = YES;
|
||||
}];
|
||||
break;
|
||||
case UIAlertViewStyleLoginAndPasswordInput:
|
||||
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||||
textField.placeholder = RCTUIKitLocalizedString(@"Login");
|
||||
}];
|
||||
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||||
textField.placeholder = RCTUIKitLocalizedString(@"Password");
|
||||
textField.secureTextEntry = YES;
|
||||
}];
|
||||
case UIAlertViewStyleDefault:
|
||||
break;
|
||||
}
|
||||
|
||||
for (NSDictionary *button in buttons) {
|
||||
alertController.message = message;
|
||||
|
||||
for (NSDictionary<NSString *, id> *button in buttons) {
|
||||
if (button.count != 1) {
|
||||
RCTLogError(@"Button definitions should have exactly one key.");
|
||||
}
|
||||
NSString *buttonKey = button.allKeys.firstObject;
|
||||
NSString *buttonTitle = [button[buttonKey] description];
|
||||
UIAlertActionStyle buttonStyle = [buttonKey isEqualToString:@"cancel"] ? UIAlertActionStyleCancel : UIAlertActionStyleDefault;
|
||||
UITextField *textField = allowsTextInput ? alertController.textFields.firstObject : nil;
|
||||
NSString *buttonTitle = [RCTConvert NSString:button[buttonKey]];
|
||||
UIAlertActionStyle buttonStyle = UIAlertActionStyleDefault;
|
||||
if ([buttonKey isEqualToString:cancelButtonKey]) {
|
||||
buttonStyle = UIAlertActionStyleCancel;
|
||||
} else if ([buttonKey isEqualToString:destructiveButtonKey]) {
|
||||
buttonStyle = UIAlertActionStyleDestructive;
|
||||
}
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:buttonTitle
|
||||
style:buttonStyle
|
||||
handler:^(__unused UIAlertAction *action) {
|
||||
if (callback) {
|
||||
if (allowsTextInput) {
|
||||
callback(@[buttonKey, textField.text]);
|
||||
} else {
|
||||
callback(@[buttonKey]);
|
||||
switch (type) {
|
||||
case UIAlertViewStylePlainTextInput:
|
||||
case UIAlertViewStyleSecureTextInput:
|
||||
callback(@[buttonKey, [alertController.textFields.firstObject text]]);
|
||||
break;
|
||||
case UIAlertViewStyleLoginAndPasswordInput: {
|
||||
NSDictionary<NSString *, NSString *> *loginCredentials = @{
|
||||
@"login": [alertController.textFields.firstObject text],
|
||||
@"password": [alertController.textFields.lastObject text]
|
||||
};
|
||||
callback(@[buttonKey, loginCredentials]);
|
||||
break;
|
||||
}
|
||||
case UIAlertViewStyleDefault:
|
||||
callback(@[buttonKey]);
|
||||
break;
|
||||
}
|
||||
}]];
|
||||
}
|
||||
|
@ -188,10 +242,22 @@ RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args
|
|||
RCTResponseSenderBlock callback = _alertCallbacks[index];
|
||||
NSArray<NSString *> *buttonKeys = _alertButtonKeys[index];
|
||||
|
||||
if (alertView.alertViewStyle == UIAlertViewStylePlainTextInput) {
|
||||
switch (alertView.alertViewStyle) {
|
||||
case UIAlertViewStylePlainTextInput:
|
||||
case UIAlertViewStyleSecureTextInput:
|
||||
callback(@[buttonKeys[buttonIndex], [alertView textFieldAtIndex:0].text]);
|
||||
} else {
|
||||
break;
|
||||
case UIAlertViewStyleLoginAndPasswordInput: {
|
||||
NSDictionary<NSString *, NSString *> *loginCredentials = @{
|
||||
@"login": [alertView textFieldAtIndex:0].text,
|
||||
@"password": [alertView textFieldAtIndex:1].text,
|
||||
};
|
||||
callback(@[buttonKeys[buttonIndex], loginCredentials]);
|
||||
break;
|
||||
}
|
||||
case UIAlertViewStyleDefault:
|
||||
callback(@[buttonKeys[buttonIndex]]);
|
||||
break;
|
||||
}
|
||||
|
||||
[_alerts removeObjectAtIndex:index];
|
||||
|
|
Loading…
Reference in New Issue