Unverified Commit c06bf650 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

iOS smart quote/dash configuration (#44923)

smartDashesType and smartQuotesType params for text fields to control iOS's smart punctuation feature.
parent 1bca434c
......@@ -12,7 +12,7 @@ import 'icons.dart';
import 'text_selection.dart';
import 'theme.dart';
export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization;
export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization, SmartQuotesType, SmartDashesType;
// Value inspected from Xcode 11 & iOS 13.0 Simulator.
const BorderSide _kDefaultRoundedBorderSide = BorderSide(
......@@ -198,8 +198,8 @@ class CupertinoTextField extends StatefulWidget {
///
/// The [autocorrect], [autofocus], [clearButtonMode], [dragStartBehavior],
/// [expands], [maxLengthEnforced], [obscureText], [prefixMode], [readOnly],
/// [scrollPadding], [suffixMode], [textAlign], and [enableSuggestions] properties
/// must not be null.
/// [scrollPadding], [suffixMode], [textAlign], and [enableSuggestions]
/// properties must not be null.
///
/// See also:
///
......@@ -236,6 +236,8 @@ class CupertinoTextField extends StatefulWidget {
this.autofocus = false,
this.obscureText = false,
this.autocorrect = true,
SmartDashesType smartDashesType,
SmartQuotesType smartQuotesType,
this.enableSuggestions = true,
this.maxLines = 1,
this.minLines,
......@@ -262,6 +264,8 @@ class CupertinoTextField extends StatefulWidget {
assert(autofocus != null),
assert(obscureText != null),
assert(autocorrect != null),
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
assert(enableSuggestions != null),
assert(maxLengthEnforced != null),
assert(scrollPadding != null),
......@@ -418,6 +422,12 @@ class CupertinoTextField extends StatefulWidget {
/// {@macro flutter.widgets.editableText.autocorrect}
final bool autocorrect;
/// {@macro flutter.services.textInput.smartDashesType}
final SmartDashesType smartDashesType;
/// {@macro flutter.services.textInput.smartQuotesType}
final SmartQuotesType smartQuotesType;
/// {@macro flutter.services.textInput.enableSuggestions}
final bool enableSuggestions;
......@@ -568,6 +578,8 @@ class CupertinoTextField extends StatefulWidget {
properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
properties.add(EnumProperty<SmartQuotesType>('smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
properties.add(DiagnosticsProperty<bool>('enableSuggestions', enableSuggestions, defaultValue: true));
properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
properties.add(IntProperty('minLines', minLines, defaultValue: null));
......@@ -884,6 +896,8 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
autofocus: widget.autofocus,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
smartDashesType: widget.smartDashesType,
smartQuotesType: widget.smartQuotesType,
enableSuggestions: widget.enableSuggestions,
maxLines: widget.maxLines,
minLines: widget.minLines,
......
......@@ -238,4 +238,3 @@ class Factory<T> {
return 'Factory(type: $type)';
}
}
......@@ -18,7 +18,7 @@ import 'selectable_text.dart' show iOSHorizontalOffset;
import 'text_selection.dart';
import 'theme.dart';
export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization;
export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization, SmartQuotesType, SmartDashesType;
/// Signature for the [TextField.buildCounter] callback.
typedef InputCounterWidgetBuilder = Widget Function(
......@@ -280,8 +280,8 @@ class TextField extends StatefulWidget {
/// is null (the default) and [readOnly] is true.
///
/// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect],
/// [maxLengthEnforced], [scrollPadding], [maxLines], [maxLength], and
/// [enableSuggestions] arguments must not be null.
/// [maxLengthEnforced], [scrollPadding], [maxLines], [maxLength],
/// and [enableSuggestions] arguments must not be null.
///
/// See also:
///
......@@ -306,6 +306,8 @@ class TextField extends StatefulWidget {
this.autofocus = false,
this.obscureText = false,
this.autocorrect = true,
SmartDashesType smartDashesType,
SmartQuotesType smartQuotesType,
this.enableSuggestions = true,
this.maxLines = 1,
this.minLines,
......@@ -333,6 +335,8 @@ class TextField extends StatefulWidget {
assert(autofocus != null),
assert(obscureText != null),
assert(autocorrect != null),
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
assert(enableSuggestions != null),
assert(enableInteractiveSelection != null),
assert(maxLengthEnforced != null),
......@@ -459,6 +463,12 @@ class TextField extends StatefulWidget {
/// {@macro flutter.widgets.editableText.autocorrect}
final bool autocorrect;
/// {@macro flutter.services.textInput.smartDashesType}
final SmartDashesType smartDashesType;
/// {@macro flutter.services.textInput.smartQuotesType}
final SmartQuotesType smartQuotesType;
/// {@macro flutter.services.textInput.enableSuggestions}
final bool enableSuggestions;
......@@ -684,6 +694,8 @@ class TextField extends StatefulWidget {
properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
properties.add(EnumProperty<SmartQuotesType>('smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
properties.add(DiagnosticsProperty<bool>('enableSuggestions', enableSuggestions, defaultValue: true));
properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
properties.add(IntProperty('minLines', minLines, defaultValue: null));
......@@ -966,6 +978,8 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
autofocus: widget.autofocus,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
smartDashesType: widget.smartDashesType,
smartQuotesType: widget.smartQuotesType,
enableSuggestions: widget.enableSuggestions,
maxLines: widget.maxLines,
minLines: widget.minLines,
......
......@@ -9,6 +9,8 @@ import 'input_decorator.dart';
import 'text_field.dart';
import 'theme.dart';
export 'package:flutter/services.dart' show SmartQuotesType, SmartDashesType;
/// A [FormField] that contains a [TextField].
///
/// This is a convenience widget that wraps a [TextField] widget in a
......@@ -98,6 +100,8 @@ class TextFormField extends FormField<String> {
bool showCursor,
bool obscureText = false,
bool autocorrect = true,
SmartDashesType smartDashesType,
SmartQuotesType smartQuotesType,
bool enableSuggestions = true,
bool autovalidate = false,
bool maxLengthEnforced = true,
......@@ -179,6 +183,8 @@ class TextFormField extends FormField<String> {
showCursor: showCursor,
obscureText: obscureText,
autocorrect: autocorrect,
smartDashesType: smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType: smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
enableSuggestions: enableSuggestions,
maxLengthEnforced: maxLengthEnforced,
maxLines: maxLines,
......
......@@ -27,6 +27,54 @@ export 'dart:ui' show TextAffinity;
// Whether we're compiled to JavaScript in a web browser.
const bool _kIsBrowser = identical(0, 0.0);
/// Indicates how to handle the intelligent replacement of dashes in text input.
///
/// See also:
///
/// * [TextField.smartDashesType]
/// * [TextFormField.smartDashesType]
/// * [CupertinoTextField.smartDashesType]
/// * [EditableText.smartDashesType]
/// * [SmartQuotesType]
/// * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
enum SmartDashesType {
/// Smart dashes is disabled.
///
/// This corresponds to the
/// ["no" value of UITextSmartDashesType](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/no).
disabled,
/// Smart dashes is enabled.
///
/// This corresponds to the
/// ["yes" value of UITextSmartDashesType](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/yes).
enabled,
}
/// Indicates how to handle the intelligent replacement of quotes in text input.
///
/// See also:
///
/// * [TextField.smartQuotesType]
/// * [TextFormField.smartQuotesType]
/// * [CupertinoTextField.smartQuotesType]
/// * [EditableText.smartQuotesType]
/// * [SmartDashesType]
/// * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
enum SmartQuotesType {
/// Smart quotes is disabled.
///
/// This corresponds to the
/// ["no" value of UITextSmartQuotesType](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/no).
disabled,
/// Smart quotes is enabled.
///
/// This corresponds to the
/// ["yes" value of UITextSmartQuotesType](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/yes).
enabled,
}
/// The type of information for which to optimize the text input control.
///
/// On Android, behavior may vary across device and keyboard provider.
......@@ -385,6 +433,8 @@ class TextInputConfiguration {
this.inputType = TextInputType.text,
this.obscureText = false,
this.autocorrect = true,
SmartDashesType smartDashesType,
SmartQuotesType smartQuotesType,
this.enableSuggestions = true,
this.actionLabel,
this.inputAction = TextInputAction.done,
......@@ -392,6 +442,8 @@ class TextInputConfiguration {
this.textCapitalization = TextCapitalization.none,
}) : assert(inputType != null),
assert(obscureText != null),
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
assert(autocorrect != null),
assert(enableSuggestions != null),
assert(keyboardAppearance != null),
......@@ -411,6 +463,56 @@ class TextInputConfiguration {
/// Defaults to true.
final bool autocorrect;
/// {@template flutter.services.textInput.smartDashesType}
/// Whether to allow the platform to automatically format dashes.
///
/// This flag only affects iOS versions 11 and above. It sets
/// [`UITextSmartDashesType`](https://developer.apple.com/documentation/uikit/uitextsmartdashestype?language=objc)
/// in the engine. When true, it passes
/// [`UITextSmartDashesTypeYes`](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/uitextsmartdashestypeyes?language=objc),
/// and when false, it passes
/// [`UITextSmartDashesTypeNo`](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/uitextsmartdashestypeno?language=objc).
///
/// As an example of what this does, two consecutive hyphen characters will be
/// automatically replaced with one en dash, and three consecutive hyphens
/// will become one em dash.
///
/// Defaults to true, unless [obscureText] is true, when it defaults to false.
/// This is to avoid the problem where password fields receive autoformatted
/// characters.
///
/// See also:
///
/// * [smartQuotesType]
/// * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
/// {@endtemplate}
final SmartDashesType smartDashesType;
/// {@template flutter.services.textInput.smartQuotesType}
/// Whether to allow the platform to automatically format quotes.
///
/// This flag only affects iOS. It sets
/// [`UITextSmartQuotesType`](https://developer.apple.com/documentation/uikit/uitextsmartquotestype?language=objc)
/// in the engine. When true, it passes
/// [`UITextSmartQuotesTypeYes`](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/uitextsmartquotestypeyes?language=objc),
/// and when false, it passes
/// [`UITextSmartQuotesTypeNo`](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/uitextsmartquotestypeno?language=objc).
///
/// As an example of what this does, a standard vertical double quote
/// character will be automatically replaced by a left or right double quote
/// depending on its position in a word.
///
/// Defaults to true, unless [obscureText] is true, when it defaults to false.
/// This is to avoid the problem where password fields receive autoformatted
/// characters.
///
/// See also:
///
/// * [smartDashesType]
/// * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
/// {@endtemplate}
final SmartQuotesType smartQuotesType;
/// {@template flutter.services.textInput.enableSuggestions}
/// Whether to show input suggestions as the user types.
///
......@@ -455,6 +557,8 @@ class TextInputConfiguration {
'inputType': inputType.toJson(),
'obscureText': obscureText,
'autocorrect': autocorrect,
'smartDashesType': smartDashesType.index.toString(),
'smartQuotesType': smartQuotesType.index.toString(),
'enableSuggestions': enableSuggestions,
'actionLabel': actionLabel,
'inputAction': inputAction.toString(),
......
......@@ -29,7 +29,7 @@ import 'scrollable.dart';
import 'text_selection.dart';
import 'ticker_provider.dart';
export 'package:flutter/services.dart' show TextEditingValue, TextSelection, TextInputType;
export 'package:flutter/services.dart' show TextEditingValue, TextSelection, TextInputType, SmartQuotesType, SmartDashesType;
export 'package:flutter/rendering.dart' show SelectionChangedCause;
/// Signature for the callback that reports when the user changes the selection
......@@ -342,9 +342,10 @@ class EditableText extends StatefulWidget {
/// The [controller], [focusNode], [obscureText], [autocorrect], [autofocus],
/// [showSelectionHandles], [enableInteractiveSelection], [forceLine],
/// [style], [cursorColor], [cursorOpacityAnimates],[backgroundCursorColor],
/// [enableSuggestions], [paintCursorAboveText], [textAlign], [dragStartBehavior],
/// [scrollPadding], [dragStartBehavior], [toolbarOptions],
/// [rendererIgnoresPointer], and [readOnly] arguments must not be null.
/// [enableSuggestions], [paintCursorAboveText], [textAlign],
/// [dragStartBehavior], [scrollPadding], [dragStartBehavior],
/// [toolbarOptions], [rendererIgnoresPointer], and [readOnly] arguments must
/// not be null.
EditableText({
Key key,
@required this.controller,
......@@ -352,6 +353,8 @@ class EditableText extends StatefulWidget {
this.readOnly = false,
this.obscureText = false,
this.autocorrect = true,
SmartDashesType smartDashesType,
SmartQuotesType smartQuotesType,
this.enableSuggestions = true,
@required this.style,
StrutStyle strutStyle,
......@@ -402,6 +405,8 @@ class EditableText extends StatefulWidget {
assert(focusNode != null),
assert(obscureText != null),
assert(autocorrect != null),
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
assert(enableSuggestions != null),
assert(showSelectionHandles != null),
assert(enableInteractiveSelection != null),
......@@ -517,6 +522,12 @@ class EditableText extends StatefulWidget {
/// {@endtemplate}
final bool autocorrect;
/// {@macro flutter.services.textInput.smartDashesType}
final SmartDashesType smartDashesType;
/// {@macro flutter.services.textInput.smartQuotesType}
final SmartQuotesType smartQuotesType;
/// {@macro flutter.services.textInput.enableSuggestions}
final bool enableSuggestions;
......@@ -1042,6 +1053,8 @@ class EditableText extends StatefulWidget {
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode));
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
properties.add(EnumProperty<SmartQuotesType>('smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
properties.add(DiagnosticsProperty<bool>('enableSuggestions', enableSuggestions, defaultValue: true));
style?.debugFillProperties(properties);
properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
......@@ -1401,6 +1414,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
inputType: widget.keyboardType,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
smartDashesType: widget.smartDashesType ?? (widget.obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType: widget.smartQuotesType ?? (widget.obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
enableSuggestions: widget.enableSuggestions,
inputAction: widget.textInputAction ?? (widget.keyboardType == TextInputType.multiline
? TextInputAction.newline
......@@ -1862,6 +1877,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
textWidthBasis: widget.textWidthBasis,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
smartDashesType: widget.smartDashesType,
smartQuotesType: widget.smartQuotesType,
enableSuggestions: widget.enableSuggestions,
offset: offset,
onSelectionChanged: _handleSelectionChanged,
......@@ -1928,6 +1945,8 @@ class _Editable extends LeafRenderObjectWidget {
this.locale,
this.obscureText,
this.autocorrect,
this.smartDashesType,
this.smartQuotesType,
this.enableSuggestions,
this.offset,
this.onSelectionChanged,
......@@ -1966,6 +1985,8 @@ class _Editable extends LeafRenderObjectWidget {
final bool obscureText;
final TextWidthBasis textWidthBasis;
final bool autocorrect;
final SmartDashesType smartDashesType;
final SmartQuotesType smartQuotesType;
final bool enableSuggestions;
final ViewportOffset offset;
final SelectionChangedHandler onSelectionChanged;
......
......@@ -6547,6 +6547,8 @@ void main() {
maxLines: 10,
maxLength: 100,
maxLengthEnforced: false,
smartDashesType: SmartDashesType.disabled,
smartQuotesType: SmartQuotesType.disabled,
enabled: false,
cursorWidth: 1.0,
cursorRadius: Radius.zero,
......@@ -6567,6 +6569,8 @@ void main() {
'style: TextStyle(inherit: true, color: Color(0xff00ff00))',
'autofocus: true',
'autocorrect: false',
'smartDashesType: disabled',
'smartQuotesType: disabled',
'maxLines: 10',
'maxLength: 100',
'maxLength not enforced',
......
......@@ -358,6 +358,101 @@ void main() {
expect(tester.testTextInput.setClientArgs['enableSuggestions'], enableSuggestions);
});
group('smartDashesType and smartQuotesType', () {
testWidgets('sent to the engine properly', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
const SmartDashesType smartDashesType = SmartDashesType.disabled;
const SmartQuotesType smartQuotesType = SmartQuotesType.disabled;
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
smartDashesType: smartDashesType,
smartQuotesType: smartQuotesType,
style: textStyle,
cursorColor: cursorColor,
),
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
await tester.idle();
expect(tester.testTextInput.setClientArgs['smartDashesType'], smartDashesType.index.toString());
expect(tester.testTextInput.setClientArgs['smartQuotesType'], smartQuotesType.index.toString());
});
testWidgets('default to true when obscureText is false', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
obscureText: false,
),
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
await tester.idle();
expect(tester.testTextInput.setClientArgs['smartDashesType'], '1');
expect(tester.testTextInput.setClientArgs['smartQuotesType'], '1');
});
testWidgets('default to false when obscureText is true', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
obscureText: true,
),
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
await tester.idle();
expect(tester.testTextInput.setClientArgs['smartDashesType'], '0');
expect(tester.testTextInput.setClientArgs['smartQuotesType'], '0');
});
});
testWidgets('selection overlay will update when text grow bigger', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController.fromValue(
const TextEditingValue(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment