Add a CupertinoTextField (#23194)
diff --git a/examples/flutter_gallery/lib/demo/cupertino/cupertino.dart b/examples/flutter_gallery/lib/demo/cupertino/cupertino.dart index 2b7e62c..d76a1e3 100644 --- a/examples/flutter_gallery/lib/demo/cupertino/cupertino.dart +++ b/examples/flutter_gallery/lib/demo/cupertino/cupertino.dart
@@ -11,3 +11,4 @@ export 'cupertino_segmented_control_demo.dart'; export 'cupertino_slider_demo.dart'; export 'cupertino_switch_demo.dart'; +export 'cupertino_text_field_demo.dart';
diff --git a/examples/flutter_gallery/lib/demo/cupertino/cupertino_text_field_demo.dart b/examples/flutter_gallery/lib/demo/cupertino/cupertino_text_field_demo.dart new file mode 100644 index 0000000..0cad56b --- /dev/null +++ b/examples/flutter_gallery/lib/demo/cupertino/cupertino_text_field_demo.dart
@@ -0,0 +1,189 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; + +class CupertinoTextFieldDemo extends StatefulWidget { + static const String routeName = '/cupertino/text_fields'; + + @override + _CupertinoTextFieldDemoState createState() { + return _CupertinoTextFieldDemoState(); + } +} + +class _CupertinoTextFieldDemoState extends State<CupertinoTextFieldDemo> { + TextEditingController _chatTextController; + TextEditingController _locationTextController; + + @override + void initState() { + super.initState(); + _chatTextController = TextEditingController(); + _locationTextController = TextEditingController(text: 'Montreal, Canada'); + } + + Widget _buildChatTextField() { + return CupertinoTextField( + controller: _chatTextController, + textCapitalization: TextCapitalization.sentences, + placeholder: 'Text Message', + decoration: BoxDecoration( + border: Border.all( + width: 0.0, + color: CupertinoColors.inactiveGray, + ), + borderRadius: BorderRadius.circular(15.0), + ), + maxLines: null, + keyboardType: TextInputType.multiline, + prefix: const Padding(padding: EdgeInsets.symmetric(horizontal: 4.0)), + suffix: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: CupertinoButton( + color: CupertinoColors.activeGreen, + minSize: 0.0, + child: const Icon( + CupertinoIcons.up_arrow, + size: 21.0, + color: CupertinoColors.white, + ), + padding: const EdgeInsets.all(2.0), + borderRadius: BorderRadius.circular(15.0), + onPressed: ()=> setState(()=> _chatTextController.clear()), + ), + ), + autofocus: true, + suffixMode: OverlayVisibilityMode.editing, + onSubmitted: (String text)=> setState(()=> _chatTextController.clear()), + ); + } + + Widget _buildNameField() { + return const CupertinoTextField( + prefix: Icon( + CupertinoIcons.person_solid, + color: CupertinoColors.lightBackgroundGray, + size: 28.0, + ), + padding: EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0), + clearButtonMode: OverlayVisibilityMode.editing, + textCapitalization: TextCapitalization.words, + autocorrect: false, + decoration: BoxDecoration( + border: Border(bottom: BorderSide(width: 0.0, color: CupertinoColors.inactiveGray)), + ), + placeholder: 'Name', + ); + } + + Widget _buildEmailField() { + return const CupertinoTextField( + prefix: Icon( + CupertinoIcons.mail_solid, + color: CupertinoColors.lightBackgroundGray, + size: 28.0, + ), + padding: EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0), + clearButtonMode: OverlayVisibilityMode.editing, + keyboardType: TextInputType.emailAddress, + autocorrect: false, + decoration: BoxDecoration( + border: Border(bottom: BorderSide(width: 0.0, color: CupertinoColors.inactiveGray)), + ), + placeholder: 'Email', + ); + } + + Widget _buildLocationField() { + return CupertinoTextField( + controller: _locationTextController, + prefix: const Icon( + CupertinoIcons.location_solid, + color: CupertinoColors.lightBackgroundGray, + size: 28.0, + ), + padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0), + clearButtonMode: OverlayVisibilityMode.editing, + textCapitalization: TextCapitalization.words, + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(width: 0.0, color: CupertinoColors.inactiveGray)), + ), + placeholder: 'Location', + ); + } + + Widget _buildPinField() { + return const CupertinoTextField( + prefix: Icon( + CupertinoIcons.padlock_solid, + color: CupertinoColors.lightBackgroundGray, + size: 28.0, + ), + padding: EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0), + clearButtonMode: OverlayVisibilityMode.editing, + keyboardType: TextInputType.number, + autocorrect: false, + obscureText: true, + decoration: BoxDecoration( + border: Border(bottom: BorderSide(width: 0.0, color: CupertinoColors.inactiveGray)), + ), + placeholder: 'Create a PIN', + ); + } + + Widget _buildTagsField() { + return CupertinoTextField( + controller: TextEditingController(text: 'colleague, reading club'), + prefix: const Icon( + CupertinoIcons.tags_solid, + color: CupertinoColors.lightBackgroundGray, + size: 28.0, + ), + enabled: false, + padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0), + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(width: 0.0, color: CupertinoColors.inactiveGray)), + ), + ); + } + + @override + Widget build(BuildContext context) { + return DefaultTextStyle( + style: const TextStyle( + fontFamily: '.SF UI Text', + inherit: false, + fontSize: 17.0, + color: CupertinoColors.black, + ), + child: CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar( + previousPageTitle: 'Cupertino', + middle: Text('Text Fields'), + ), + child: ListView( + children: <Widget>[ + Padding( + padding: const EdgeInsets.symmetric(vertical: 32.0, horizontal: 16.0), + child: Column( + children: <Widget>[ + _buildNameField(), + _buildEmailField(), + _buildLocationField(), + _buildPinField(), + _buildTagsField(), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 32.0, horizontal: 16.0), + child: _buildChatTextField(), + ), + ], + ), + ), + ); + } +}
diff --git a/examples/flutter_gallery/lib/gallery/demos.dart b/examples/flutter_gallery/lib/gallery/demos.dart index 2f1064a..0b18552 100644 --- a/examples/flutter_gallery/lib/gallery/demos.dart +++ b/examples/flutter_gallery/lib/gallery/demos.dart
@@ -500,6 +500,13 @@ documentationUrl: 'https://docs.flutter.io/flutter/cupertino/CupertinoSwitch-class.html', buildRoute: (BuildContext context) => CupertinoSwitchDemo(), ), + GalleryDemo( + title: 'Text Fields', + icon: GalleryIcons.text_fields_alt, + category: _kCupertinoComponents, + routeName: CupertinoTextFieldDemo.routeName, + buildRoute: (BuildContext context) => CupertinoTextFieldDemo(), + ), // Media GalleryDemo(
diff --git a/packages/flutter/lib/cupertino.dart b/packages/flutter/lib/cupertino.dart index 792e88c..473d91a 100644 --- a/packages/flutter/lib/cupertino.dart +++ b/packages/flutter/lib/cupertino.dart
@@ -28,6 +28,7 @@ export 'src/cupertino/switch.dart'; export 'src/cupertino/tab_scaffold.dart'; export 'src/cupertino/tab_view.dart'; +export 'src/cupertino/text_field.dart'; export 'src/cupertino/text_selection.dart'; export 'src/cupertino/thumb_painter.dart'; export 'widgets.dart';
diff --git a/packages/flutter/lib/src/cupertino/app.dart b/packages/flutter/lib/src/cupertino/app.dart index c951e53..833b39c 100644 --- a/packages/flutter/lib/src/cupertino/app.dart +++ b/packages/flutter/lib/src/cupertino/app.dart
@@ -243,7 +243,7 @@ _navigatorObservers = List<NavigatorObserver>.from(widget.navigatorObservers) ..add(_heroController); } else { - _navigatorObservers = null; + _navigatorObservers = const <NavigatorObserver>[]; } }
diff --git a/packages/flutter/lib/src/cupertino/icons.dart b/packages/flutter/lib/src/cupertino/icons.dart index 51d805a..6bb0f88 100644 --- a/packages/flutter/lib/src/cupertino/icons.dart +++ b/packages/flutter/lib/src/cupertino/icons.dart
@@ -721,4 +721,44 @@ /// * [group], which is similar, but not filled in. /// * [person_solid], which is just a single person. static const IconData group_solid = IconData(0xf47c, fontFamily: iconFont, fontPackage: iconFontPackage); + + /// Outline of a closed mail envelope. + static const IconData mail = IconData(0xf422, fontFamily: iconFont, fontPackage: iconFontPackage); + + /// A closed mail envelope. This icon is filled in. + static const IconData mail_solid = IconData(0xf423, fontFamily: iconFont, fontPackage: iconFontPackage); + + /// Outline of a location pin. + static const IconData location = IconData(0xf455, fontFamily: iconFont, fontPackage: iconFontPackage); + + /// A location pin. This icon is filled in. + static const IconData location_solid = IconData(0xf456, fontFamily: iconFont, fontPackage: iconFontPackage); + + /// Outline of a sticker tag. + /// + /// See also: + /// + /// * [tags], similar but with 2 overlapping tags. + static const IconData tag = IconData(0xf48c, fontFamily: iconFont, fontPackage: iconFontPackage); + + /// A sticker tag. This icon is filled in. + /// + /// See also: + /// + /// * [tags_solid], similar but with 2 overlapping tags. + static const IconData tag_solid = IconData(0xf48d, fontFamily: iconFont, fontPackage: iconFontPackage); + + /// Outlines of 2 overlapping sticker tags. + /// + /// See also: + /// + /// * [tag], similar but with only one tag. + static const IconData tags = IconData(0xf48e, fontFamily: iconFont, fontPackage: iconFontPackage); + + /// 2 overlapping sticker tags. This icon is filled in. + /// + /// See also: + /// + /// * [tag_solid], similar but with only one tag. + static const IconData tags_solid = IconData(0xf48f, fontFamily: iconFont, fontPackage: iconFontPackage); }
diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart new file mode 100644 index 0000000..9e5b8ca --- /dev/null +++ b/packages/flutter/lib/src/cupertino/text_field.dart
@@ -0,0 +1,660 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import 'colors.dart'; +import 'icons.dart'; +import 'text_selection.dart'; + +export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization; + +// Value extracted via color reader from iOS simulator. +const BorderSide _kDefaultRoundedBorderSide = BorderSide( + color: CupertinoColors.lightBackgroundGray, + style: BorderStyle.solid, + width: 0.0, +); +const Border _kDefaultRoundedBorder = Border( + top: _kDefaultRoundedBorderSide, + bottom: _kDefaultRoundedBorderSide, + left: _kDefaultRoundedBorderSide, + right: _kDefaultRoundedBorderSide, +); +// Counted manually on magnified simulator. +const BoxDecoration _kDefaultRoundedBorderDecoration = BoxDecoration( + border: _kDefaultRoundedBorder, + borderRadius: BorderRadius.all(Radius.circular(4.0)), +); + +// Default iOS style from HIG specs with larger font. +const TextStyle _kDefaultTextStyle = TextStyle( + fontFamily: '.SF Pro Text', + fontSize: 17.0, + letterSpacing: -0.38, + color: CupertinoColors.black, + decoration: TextDecoration.none, +); + +// Value extracted via color reader from iOS simulator. +const Color _kSelectionHighlightColor = Color(0x667FAACF); +const Color _kInactiveTextColor = Color(0xFFC2C2C2); +const Color _kDisabledBackground = Color(0xFFFAFAFA); + +/// Visibility of text field overlays based on the state of the current text entry. +/// +/// Used to toggle the visibility behavior of the optional decorating widgets +/// surrounding the [EditableText] such as the clear text button. +enum OverlayVisibilityMode { + /// Overlay will never appear regardless of the text entry state. + never, + + /// Overlay will only appear when the current text entry is not empty. + /// + /// This includes pre-filled text that the user did not type in manually. But + /// does not include text in placeholders. + editing, + + /// Overlay will only appear when the current text entry is empty. + /// + /// This also includes not having pre-filled text that the user did not type + /// in manually. Texts in placeholders are ignored. + notEditing, + + /// Always show the overlay regardless of the text entry state. + always, +} + +/// An iOS-style text field. +/// +/// A text field lets the user enter text, either with a hardware keyboard or with +/// an onscreen keyboard. +/// +/// This widget corresponds to both a `UITextField` and an editable `UITextView` +/// on iOS. +/// +/// The text field calls the [onChanged] callback whenever the user changes the +/// text in the field. If the user indicates that they are done typing in the +/// field (e.g., by pressing a button on the soft keyboard), the text field +/// calls the [onSubmitted] callback. +/// +/// To control the text that is displayed in the text field, use the +/// [controller]. For example, to set the initial value of the text field, use +/// a [controller] that already contains some text such as: +/// +/// ## Sample code +/// +/// ```dart +/// class MyPrefilledText extends StatefulWidget { +/// @override +/// _MyPrefilledTextState createState() => _MyPrefilledTextState(); +/// } +/// +/// class _MyPrefilledTextState extends State<MyPrefilledText> { +/// TextEditingController _textController; +/// +/// @override +/// void initState() { +/// super.initState(); +/// _textController = TextEditingController(text: 'initial text'); +/// } +/// +/// @override +/// Widget build(BuildContext context) { +/// return CupertinoTextField(controller: _textController); +/// } +/// } +/// ``` +/// +/// The [controller] can also control the selection and composing region (and to +/// observe changes to the text, selection, and composing region). +/// +/// The text field has an overridable [decoration] that, by default, draws a +/// rounded rectangle border around the text field. If you set the [decoration] +/// property to null, the decoration will be removed entirely. +/// +/// See also: +/// +/// * <https://developer.apple.com/documentation/uikit/uitextfield> +/// * [TextField], an alternative text field widget that follows the Material +/// Design UI conventions. +/// * [EditableText], which is the raw text editing control at the heart of a +/// [TextField]. +class CupertinoTextField extends StatefulWidget { + /// Creates an iOS-style text field. + /// + /// To provide a prefilled text entry, pass in a [TextEditingController] with + /// an initial value to the [controller] parameter. + /// + /// To provide a hint placeholder text that appears when the text entry is + /// empty, pass a [String] to the [placeholder] parameter. + /// + /// The [maxLines] property can be set to null to remove the restriction on + /// the number of lines. In this mode, the intrinsic height of the widget will + /// grow as the number of lines of text grows. By default, it is `1`, meaning + /// this is a single-line text field and will scroll horizontally when + /// overflown. [maxLines] must not be zero. + /// + /// See also: + /// + /// * [maxLength], which discusses the precise meaning of "number of + /// characters" and how it may differ from the intuitive meaning. + const CupertinoTextField({ + Key key, + this.controller, + this.focusNode, + this.decoration = _kDefaultRoundedBorderDecoration, + this.padding = const EdgeInsets.all(6.0), + this.placeholder, + this.prefix, + this.prefixMode = OverlayVisibilityMode.always, + this.suffix, + this.suffixMode = OverlayVisibilityMode.always, + this.clearButtonMode = OverlayVisibilityMode.never, + TextInputType keyboardType, + this.textInputAction, + this.textCapitalization = TextCapitalization.none, + this.style = _kDefaultTextStyle, + this.textAlign = TextAlign.start, + this.autofocus = false, + this.obscureText = false, + this.autocorrect = true, + this.maxLines = 1, + this.maxLength, + this.maxLengthEnforced = true, + this.onChanged, + this.onEditingComplete, + this.onSubmitted, + this.inputFormatters, + this.enabled, + this.cursorWidth = 2.0, + this.cursorRadius, + this.cursorColor = CupertinoColors.activeBlue, + this.keyboardAppearance, + this.scrollPadding = const EdgeInsets.all(20.0), + }) : assert(textAlign != null), + assert(autofocus != null), + assert(obscureText != null), + assert(autocorrect != null), + assert(maxLengthEnforced != null), + assert(scrollPadding != null), + assert(maxLines == null || maxLines > 0), + assert(maxLength == null || maxLength > 0), + assert(clearButtonMode != null), + assert(prefixMode != null), + assert(suffixMode != null), + keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), + super(key: key); + + /// Controls the text being edited. + /// + /// If null, this widget will create its own [TextEditingController]. + final TextEditingController controller; + + /// Controls whether this widget has keyboard focus. + /// + /// If null, this widget will create its own [FocusNode]. + final FocusNode focusNode; + + /// Controls the [BoxDecoration] of the box behind the text input. + /// + /// Defaults to having a rounded rectangle grey border and can be null to have + /// no box decoration. + final BoxDecoration decoration; + + /// Padding around the text entry area between the [prefix] and [suffix] + /// or the clear button when [clearButtonMode] is not never. + /// + /// Defaults to a padding of 6 pixels on all sides and can be null. + final EdgeInsetsGeometry padding; + + /// A lighter colored placeholder hint that appears on the first line of the + /// text field when the text entry is empty. + /// + /// Defaults to having no placeholder text. + /// + /// The text style of the placeholder text matches that of the text field's + /// main text entry except a lighter font weight and a grey font color. + final String placeholder; + + /// An optional [Widget] to display before the text. + final Widget prefix; + + /// Controls the visibility of the [prefix] widget based on the state of + /// text entry when the [prefix] argument is not null. + /// + /// Defaults to [OverlayVisibilityMode.always] and cannot be null. + /// + /// Has no effect when [prefix] is null. + final OverlayVisibilityMode prefixMode; + + /// An optional [Widget] to display after the text. + final Widget suffix; + + /// Controls the visibility of the [suffix] widget based on the state of + /// text entry when the [suffix] argument is not null. + /// + /// Defaults to [OverlayVisibilityMode.always] and cannot be null. + /// + /// Has no effect when [suffix] is null. + final OverlayVisibilityMode suffixMode; + + /// Show an iOS-style clear button to clear the current text entry. + /// + /// Can be made to appear depending on various text states of the + /// [TextEditingController]. + /// + /// Will only appear if no [suffix] widget is appearing. + /// + /// Defaults to never appearing and cannot be null. + final OverlayVisibilityMode clearButtonMode; + + /// {@macro flutter.widgets.editableText.keyboardType} + final TextInputType keyboardType; + + /// The type of action button to use for the keyboard. + /// + /// Defaults to [TextInputAction.newline] if [keyboardType] is + /// [TextInputType.multiline] and [TextInputAction.done] otherwise. + final TextInputAction textInputAction; + + /// {@macro flutter.widgets.editableText.textCapitalization} + final TextCapitalization textCapitalization; + + /// The style to use for the text being edited. + /// + /// Also serves as a base for the [placeholder] text's style. + /// + /// Defaults to a standard iOS style and cannot be null. + final TextStyle style; + + /// {@macro flutter.widgets.editableText.textAlign} + final TextAlign textAlign; + + /// {@macro flutter.widgets.editableText.autofocus} + final bool autofocus; + + /// {@macro flutter.widgets.editableText.obscureText} + final bool obscureText; + + /// {@macro flutter.widgets.editableText.autocorrect} + final bool autocorrect; + + /// {@macro flutter.widgets.editableText.maxLines} + final int maxLines; + + /// The maximum number of characters (Unicode scalar values) to allow in the + /// text field. + /// + /// If set, a character counter will be displayed below the + /// field, showing how many characters have been entered and how many are + /// allowed. After [maxLength] characters have been input, additional input + /// is ignored, unless [maxLengthEnforced] is set to false. The TextField + /// enforces the length with a [LengthLimitingTextInputFormatter], which is + /// evaluated after the supplied [inputFormatters], if any. + /// + /// This value must be either null or greater than zero. If set to null + /// (the default), there is no limit to the number of characters allowed. + /// + /// Whitespace characters (e.g. newline, space, tab) are included in the + /// character count. + /// + /// If [maxLengthEnforced] is set to false, then more than [maxLength] + /// characters may be entered, but the error counter and divider will + /// switch to the [decoration.errorStyle] when the limit is exceeded. + /// + /// ## Limitations + /// + /// The CupertinoTextField does not currently count Unicode grapheme clusters + /// (i.e. characters visible to the user), it counts Unicode scalar values, + /// which leaves out a number of useful possible characters (like many emoji + /// and composed characters), so this will be inaccurate in the presence of + /// those characters. If you expect to encounter these kinds of characters, be + /// generous in the maxLength used. + /// + /// For instance, the character "ö" can be represented as '\u{006F}\u{0308}', + /// which is the letter "o" followed by a composed diaeresis "¨", or it can + /// be represented as '\u{00F6}', which is the Unicode scalar value "LATIN + /// SMALL LETTER O WITH DIAERESIS". In the first case, the text field will + /// count two characters, and the second case will be counted as one + /// character, even though the user can see no difference in the input. + /// + /// Similarly, some emoji are represented by multiple scalar values. The + /// Unicode "THUMBS UP SIGN + MEDIUM SKIN TONE MODIFIER", "👍🏽", should be + /// counted as a single character, but because it is a combination of two + /// Unicode scalar values, '\u{1F44D}\u{1F3FD}', it is counted as two + /// characters. + /// + /// See also: + /// + /// * [LengthLimitingTextInputFormatter] for more information on how it + /// counts characters, and how it may differ from the intuitive meaning. + final int maxLength; + + /// If true, prevents the field from allowing more than [maxLength] + /// characters. + /// + /// If [maxLength] is set, [maxLengthEnforced] indicates whether or not to + /// enforce the limit, or merely provide a character counter and warning when + /// [maxLength] is exceeded. + final bool maxLengthEnforced; + + /// {@macro flutter.widgets.editableText.onChanged} + final ValueChanged<String> onChanged; + + /// {@macro flutter.widgets.editableText.onEditingComplete} + final VoidCallback onEditingComplete; + + /// {@macro flutter.widgets.editableText.onSubmitted} + final ValueChanged<String> onSubmitted; + + /// {@macro flutter.widgets.editableText.inputFormatters} + final List<TextInputFormatter> inputFormatters; + + /// Disables the text field when false. + /// + /// Text fields in disabled states have a light grey background and don't + /// respond to touch events including the [prefix], [suffix] and the clear + /// button. + final bool enabled; + + /// {@macro flutter.widgets.editableText.cursorWidth} + final double cursorWidth; + + /// {@macro flutter.widgets.editableText.cursorRadius} + final Radius cursorRadius; + + /// The color to use when painting the cursor. + /// + /// Defaults to the standard iOS blue color. Cannot be null. + final Color cursorColor; + + /// The appearance of the keyboard. + /// + /// This setting is only honored on iOS devices. + /// + /// If null, defaults to [Brightness.light]. + final Brightness keyboardAppearance; + + /// {@macro flutter.widgets.editableText.scrollPadding} + final EdgeInsets scrollPadding; + + @override + _CupertinoTextFieldState createState() => _CupertinoTextFieldState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + + properties.add(DiagnosticsProperty<TextEditingController>('controller', controller, defaultValue: null)); + properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null)); + properties.add(DiagnosticsProperty<BoxDecoration>('decoration', decoration)); + properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding)); + properties.add(StringProperty('placeholder', placeholder)); + properties.add(DiagnosticsProperty<OverlayVisibilityMode>('prefix', prefix == null ? null : prefixMode)); + properties.add(DiagnosticsProperty<OverlayVisibilityMode>('suffix', suffix == null ? null : suffixMode)); + properties.add(DiagnosticsProperty<OverlayVisibilityMode>('clearButtonMode', clearButtonMode)); + properties.add(DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: TextInputType.text)); + properties.add(DiagnosticsProperty<TextStyle>('style', style, defaultValue: null)); + properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false)); + properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false)); + properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: false)); + properties.add(IntProperty('maxLines', maxLines, defaultValue: 1)); + properties.add(IntProperty('maxLength', maxLength, defaultValue: null)); + properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, ifTrue: 'max length enforced')); + } +} + +class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticKeepAliveClientMixin { + final GlobalKey<EditableTextState> _editableTextKey = GlobalKey<EditableTextState>(); + + TextEditingController _controller; + TextEditingController get _effectiveController => widget.controller ?? _controller; + + FocusNode _focusNode; + FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); + + @override + void initState() { + super.initState(); + if (widget.controller == null) { + _controller = TextEditingController(); + _controller.addListener(updateKeepAlive); + } + } + + @override + void didUpdateWidget(CupertinoTextField oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.controller == null && oldWidget.controller != null) { + _controller = TextEditingController.fromValue(oldWidget.controller.value); + _controller.addListener(updateKeepAlive); + } else if (widget.controller != null && oldWidget.controller == null) { + _controller = null; + } + final bool isEnabled = widget.enabled ?? true; + final bool wasEnabled = oldWidget.enabled ?? true; + if (wasEnabled && !isEnabled) { + _effectiveFocusNode.unfocus(); + } + } + + @override + void dispose() { + _focusNode?.dispose(); + _controller?.removeListener(updateKeepAlive); + super.dispose(); + } + + void _requestKeyboard() { + _editableTextKey.currentState?.requestKeyboard(); + } + + RenderEditable get _renderEditable => _editableTextKey.currentState.renderEditable; + + void _handleTapDown(TapDownDetails details) { + _renderEditable.handleTapDown(details); + } + + void _handleTap() { + _renderEditable.handleTap(); + _requestKeyboard(); + } + + void _handleLongPress() { + _renderEditable.handleLongPress(); + } + + @override + bool get wantKeepAlive => _controller?.text?.isNotEmpty == true; + + bool _shouldShowAttachment({ + OverlayVisibilityMode attachment, + bool hasText, + }) { + switch (attachment) { + case OverlayVisibilityMode.never: + return false; + case OverlayVisibilityMode.always: + return true; + case OverlayVisibilityMode.editing: + return hasText; + case OverlayVisibilityMode.notEditing: + return !hasText; + } + assert(false); + return null; + } + + bool _showPrefixWidget(TextEditingValue text) { + return widget.prefix != null && _shouldShowAttachment( + attachment: widget.prefixMode, + hasText: text.text.isNotEmpty, + ); + } + + bool _showSuffixWidget(TextEditingValue text) { + return widget.suffix != null && _shouldShowAttachment( + attachment: widget.suffixMode, + hasText: text.text.isNotEmpty, + ); + } + + bool _showClearButton(TextEditingValue text) { + return _shouldShowAttachment( + attachment: widget.clearButtonMode, + hasText: text.text.isNotEmpty, + ); + } + + Widget _addTextDependentAttachments(Widget editableText) { + // If there are no surrounding widgets, just return the core editable text + // part. + if (widget.placeholder == null && + widget.clearButtonMode == OverlayVisibilityMode.never && + widget.prefix == null && + widget.suffix == null) { + return editableText; + } + + // Otherwise, listen to the current state of the text entry. + return ValueListenableBuilder<TextEditingValue>( + valueListenable: _effectiveController, + child: editableText, + builder: (BuildContext context, TextEditingValue text, Widget child) { + final List<Widget> rowChildren = <Widget>[]; + + // Insert a prefix at the front if the prefix visibility mode matches + // the current text state. + if (_showPrefixWidget(text)) { + rowChildren.add(widget.prefix); + } + + final List<Widget> stackChildren = <Widget>[]; + + // In the middle part, stack the placeholder on top of the main EditableText + // if needed. + if (widget.placeholder != null && text.text.isEmpty) { + stackChildren.add( + Padding( + padding: widget.padding, + child: Text( + widget.placeholder, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: widget.style.merge( + const TextStyle( + color: _kInactiveTextColor, + fontWeight: FontWeight.w300, + ), + ), + ), + ), + ); + } + + rowChildren.add(Expanded(child: Stack(children: stackChildren..add(child)))); + + // First add the explicit suffix if the suffix visibility mode matches. + if (_showSuffixWidget(text)) { + rowChildren.add(widget.suffix); + // Otherwise, try to show a clear button if its visibility mode matches. + } else if (_showClearButton(text)) { + rowChildren.add( + GestureDetector( + onTap: widget.enabled ?? true + ? () => _effectiveController.clear() + : null, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 6.0), + child: Icon( + CupertinoIcons.clear_thick_circled, + size: 18.0, + color: _kInactiveTextColor, + ), + ), + ), + ); + } + + return Row(children: rowChildren); + }, + ); + } + + @override + Widget build(BuildContext context) { + super.build(context); // See AutomaticKeepAliveClientMixin. + assert(debugCheckHasDirectionality(context)); + final Brightness keyboardAppearance = widget.keyboardAppearance; + final TextEditingController controller = _effectiveController; + final FocusNode focusNode = _effectiveFocusNode; + final List<TextInputFormatter> formatters = widget.inputFormatters ?? <TextInputFormatter>[]; + final bool enabled = widget.enabled ?? true; + if (widget.maxLength != null && widget.maxLengthEnforced) { + formatters.add(LengthLimitingTextInputFormatter(widget.maxLength)); + } + + final Widget paddedEditable = Padding( + padding: widget.padding, + child: RepaintBoundary( + child: EditableText( + key: _editableTextKey, + controller: controller, + focusNode: focusNode, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + textCapitalization: widget.textCapitalization, + style: widget.style, + textAlign: widget.textAlign, + autofocus: widget.autofocus, + obscureText: widget.obscureText, + autocorrect: widget.autocorrect, + maxLines: widget.maxLines, + selectionColor: _kSelectionHighlightColor, + selectionControls: cupertinoTextSelectionControls, + onChanged: widget.onChanged, + onEditingComplete: widget.onEditingComplete, + onSubmitted: widget.onSubmitted, + inputFormatters: formatters, + rendererIgnoresPointer: true, + cursorWidth: widget.cursorWidth, + cursorRadius: widget.cursorRadius, + cursorColor: widget.cursorColor, + scrollPadding: widget.scrollPadding, + keyboardAppearance: keyboardAppearance, + ), + ), + ); + + return Semantics( + onTap: () { + if (!controller.selection.isValid) { + controller.selection = TextSelection.collapsed(offset: controller.text.length); + } + _requestKeyboard(); + }, + child: IgnorePointer( + ignoring: !enabled, + child: Container( + decoration: widget.decoration, + // The main decoration and the disabled scrim exists separately. + child: Container( + color: enabled ? null : _kDisabledBackground, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTapDown: _handleTapDown, + onTap: _handleTap, + onLongPress: _handleLongPress, + excludeFromSemantics: true, + child: _addTextDependentAttachments(paddedEditable), + ), + ), + ), + ), + ); + } +}
diff --git a/packages/flutter/lib/src/cupertino/text_selection.dart b/packages/flutter/lib/src/cupertino/text_selection.dart index 7361877..68bc389 100644 --- a/packages/flutter/lib/src/cupertino/text_selection.dart +++ b/packages/flutter/lib/src/cupertino/text_selection.dart
@@ -20,14 +20,16 @@ const Color _kToolbarBackgroundColor = Color(0xFF2E2E2E); const Color _kToolbarDividerColor = Color(0xFFB9B9B9); -const Color _kHandlesColor = Color(0xFF146DDE); +// Read off from the output on iOS 12. This color does not vary with the +// application's theme color. +const Color _kHandlesColor = Color(0xFF136FE0); // This offset is used to determine the center of the selection during a drag. // It's slightly below the center of the text so the finger isn't entirely // covering the text being selected. const Size _kSelectionOffset = Size(20.0, 30.0); const Size _kToolbarTriangleSize = Size(18.0, 9.0); -const EdgeInsets _kToolbarButtonPadding = EdgeInsets.symmetric(vertical: 10.0, horizontal: 21.0); +const EdgeInsets _kToolbarButtonPadding = EdgeInsets.symmetric(vertical: 10.0, horizontal: 18.0); const BorderRadius _kToolbarBorderRadius = BorderRadius.all(Radius.circular(7.5)); const TextStyle _kToolbarButtonFontStyle = TextStyle( @@ -121,6 +123,7 @@ // avoid letting the triangle line up with any dividers. // https://github.com/flutter/flutter/issues/11274 triangle, + const Padding(padding: EdgeInsets.only(bottom: 10.0)), ], ); }
diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 2aabd01..6b8ba72 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart
@@ -154,10 +154,7 @@ /// extra padding introduced by the decoration to save space for the labels). final InputDecoration decoration; - /// The type of keyboard to use for editing the text. - /// - /// Defaults to [TextInputType.text] if [maxLines] is one and - /// [TextInputType.multiline] otherwise. + /// {@macro flutter.widgets.editableText.keyboardType} final TextInputType keyboardType; /// The type of action button to use for the keyboard. @@ -166,17 +163,7 @@ /// [TextInputType.multiline] and [TextInputAction.done] otherwise. final TextInputAction textInputAction; - /// Configures how the platform keyboard will select an uppercase or - /// lowercase keyboard. - /// - /// Only supports text keyboards, other keyboard types will ignore this - /// configuration. Capitalization is locale-aware. - /// - /// Defaults to [TextCapitalization.none]. Must not be null. - /// - /// See also: - /// - /// * [TextCapitalization], for a description of each capitalization behavior. + /// {@macro flutter.widgets.editableText.textCapitalization} final TextCapitalization textCapitalization; /// The style to use for the text being edited. @@ -186,42 +173,19 @@ /// If null, defaults to the `subhead` text style from the current [Theme]. final TextStyle style; - /// How the text being edited should be aligned horizontally. - /// - /// Defaults to [TextAlign.start]. + /// {@macro flutter.widgets.editableText.textAlign} final TextAlign textAlign; - /// Whether this text field should focus itself if nothing else is already - /// focused. - /// - /// If true, the keyboard will open as soon as this text field obtains focus. - /// Otherwise, the keyboard is only shown after the user taps the text field. - /// - /// Defaults to false. Cannot be null. - // See https://github.com/flutter/flutter/issues/7035 for the rationale for this - // keyboard behavior. + /// {@macro flutter.widgets.editableText.autofocus} final bool autofocus; - /// Whether to hide the text being edited (e.g., for passwords). - /// - /// When this is set to true, all the characters in the text field are - /// replaced by U+2022 BULLET characters (•). - /// - /// Defaults to false. Cannot be null. + /// {@macro flutter.widgets.editableText.obscureText} final bool obscureText; - /// Whether to enable autocorrection. - /// - /// Defaults to true. Cannot be null. + /// {@macro flutter.widgets.editableText.autocorrect} final bool autocorrect; - /// The maximum number of lines for the text to span, wrapping if necessary. - /// - /// If this is 1 (the default), the text will not wrap, but will scroll - /// horizontally instead. - /// - /// If this is null, there is no limit to the number of lines. If it is not - /// null, the value must be greater than zero. + /// {@macro flutter.widgets.editableText.maxLines} final int maxLines; /// The maximum number of characters (Unicode scalar values) to allow in the @@ -280,34 +244,16 @@ /// [maxLength] is exceeded. final bool maxLengthEnforced; - /// Called when the text being edited changes. + /// {@macro flutter.widgets.editableText.onChanged} final ValueChanged<String> onChanged; - /// Called when the user submits editable content (e.g., user presses the "done" - /// button on the keyboard). - /// - /// The default implementation of [onEditingComplete] executes 2 different - /// behaviors based on the situation: - /// - /// - When a completion action is pressed, such as "done", "go", "send", or - /// "search", the user's content is submitted to the [controller] and then - /// focus is given up. - /// - /// - When a non-completion action is pressed, such as "next" or "previous", - /// the user's content is submitted to the [controller], but focus is not - /// given up because developers may want to immediately move focus to - /// another input widget within [onSubmitted]. - /// - /// Providing [onEditingComplete] prevents the aforementioned default behavior. + /// {@macro flutter.widgets.editableText.onEditingComplete} final VoidCallback onEditingComplete; - /// Called when the user indicates that they are done editing the text in the - /// field. + /// {@macro flutter.widgets.editableText.onSubmitted} final ValueChanged<String> onSubmitted; - /// Optional input validation and formatting overrides. - /// - /// Formatters are run in the provided order when the text input changes. + /// {@macro flutter.widgets.editableText.inputFormatters} final List<TextInputFormatter> inputFormatters; /// If false the textfield is "disabled": it ignores taps and its @@ -317,16 +263,15 @@ /// [Decoration.enabled] property. final bool enabled; - /// How thick the cursor will be. - /// - /// Defaults to 2.0. + /// {@macro flutter.widgets.editableText.cursorWidth} final double cursorWidth; - /// How rounded the corners of the cursor should be. - /// By default, the cursor has a null Radius + /// {@macro flutter.widgets.editableText.cursorRadius} final Radius cursorRadius; /// The color to use when painting the cursor. + /// + /// Defaults to the theme's `cursorColor` when null. final Color cursorColor; /// The appearance of the keyboard. @@ -336,14 +281,7 @@ /// If unset, defaults to the brightness of [ThemeData.primaryColorBrightness]. final Brightness keyboardAppearance; - /// Configures padding to edges surrounding a [Scrollable] when the Textfield scrolls into view. - /// - /// When this widget receives focus and is not completely visible (for example scrolled partially - /// off the screen or overlapped by the keyboard) - /// then it will attempt to make itself visible by scrolling a surrounding [Scrollable], if one is present. - /// This value controls how far from the edges of a [Scrollable] the TextField will be positioned after the scroll. - /// - /// Defaults to EdgeInserts.all(20.0). + /// {@macro flutter.widgets.editableText.scrollPadding} final EdgeInsets scrollPadding; /// {@macro flutter.widgets.editableText.enableInteractiveSelection}
diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index da88e15..5fa444b 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart
@@ -242,22 +242,31 @@ /// Controls whether this widget has keyboard focus. final FocusNode focusNode; + /// {@template flutter.widgets.editableText.obscureText} /// Whether to hide the text being edited (e.g., for passwords). /// - /// Defaults to false. + /// When this is set to true, all the characters in the text field are + /// replaced by U+2022 BULLET characters (•). + /// + /// Defaults to false. Cannot be null. + /// {@endtemplate} final bool obscureText; + /// {@template flutter.widgets.editableText.autocorrect} /// Whether to enable autocorrection. /// - /// Defaults to true. + /// Defaults to true. Cannot be null. + /// {@endtemplate} final bool autocorrect; /// The text style to use for the editable text. final TextStyle style; + /// {@template flutter.widgets.editableText.textAlign} /// How the text should be aligned horizontally. /// - /// Defaults to [TextAlign.start]. + /// Defaults to [TextAlign.start] and cannot be null. + /// {@endtemplate} final TextAlign textAlign; /// The directionality of the text. @@ -275,6 +284,7 @@ /// Defaults to the ambient [Directionality], if any. final TextDirection textDirection; + /// {@template flutter.widgets.editableText.textCapitalization} /// Configures how the platform keyboard will select an uppercase or /// lowercase keyboard. /// @@ -286,6 +296,7 @@ /// See also: /// /// * [TextCapitalization], for a description of each capitalization behavior. + /// {@endtemplate} final TextCapitalization textCapitalization; /// Used to select a font when the same Unicode character can @@ -307,8 +318,11 @@ final double textScaleFactor; /// The color to use when painting the cursor. + /// + /// Cannot be null. final Color cursorColor; + /// {@template flutter.widgets.editableText.maxLines} /// The maximum number of lines for the text to span, wrapping if necessary. /// /// If this is 1 (the default), the text will not wrap, but will scroll @@ -316,13 +330,20 @@ /// /// If this is null, there is no limit to the number of lines. If it is not /// null, the value must be greater than zero. + /// {@endtemplate} final int maxLines; - /// Whether this input field should focus itself if nothing else is already focused. - /// If true, the keyboard will open as soon as this input obtains focus. Otherwise, - /// the keyboard is only shown after the user taps the text field. + /// {@template flutter.widgets.editableText.autofocus} + /// Whether this text field should focus itself if nothing else is already + /// focused. /// - /// Defaults to false. + /// If true, the keyboard will open as soon as this text field obtains focus. + /// Otherwise, the keyboard is only shown after the user taps the text field. + /// + /// Defaults to false. Cannot be null. + /// {@endtemplate} + // See https://github.com/flutter/flutter/issues/7035 for the rationale for this + // keyboard behavior. final bool autofocus; /// The color to use when painting the selection. @@ -331,15 +352,23 @@ /// Optional delegate for building the text selection handles and toolbar. final TextSelectionControls selectionControls; + /// {@template flutter.widgets.editableText.keyboardType} /// The type of keyboard to use for editing the text. + /// + /// Defaults to [TextInputType.text] if [maxLines] is one and + /// [TextInputType.multiline] otherwise. + /// {@endtemplate} final TextInputType keyboardType; /// The type of action button to use with the soft keyboard. final TextInputAction textInputAction; + /// {@template flutter.widgets.editableText.onChanged} /// Called when the text being edited changes. + /// {@endtemplate} final ValueChanged<String> onChanged; + /// {@template flutter.widgets.editableText.onEditingComplete} /// Called when the user submits editable content (e.g., user presses the "done" /// button on the keyboard). /// @@ -356,17 +385,24 @@ /// another input widget within [onSubmitted]. /// /// Providing [onEditingComplete] prevents the aforementioned default behavior. + /// {@endtemplate} final VoidCallback onEditingComplete; - /// Called when the user indicates that they are done editing the text in the field. + /// {@template flutter.widgets.editableText.onSubmitted} + /// Called when the user indicates that they are done editing the text in the + /// field. + /// {@endtemplate} final ValueChanged<String> onSubmitted; /// Called when the user changes the selection of text (including the cursor /// location). final SelectionChangedCallback onSelectionChanged; - /// Optional input validation and formatting overrides. Formatters are run - /// in the provided order when the text input changes. + /// {@template flutter.widgets.editableText.inputFormatters} + /// Optional input validation and formatting overrides. + /// + /// Formatters are run in the provided order when the text input changes. + /// {@endtemplate} final List<TextInputFormatter> inputFormatters; /// If true, the [RenderEditable] created by this widget will not handle @@ -375,14 +411,18 @@ /// This property is false by default. final bool rendererIgnoresPointer; + /// {@template flutter.widgets.editableText.cursorWidth} /// How thick the cursor will be. /// /// Defaults to 2.0 + /// {@endtemplate} final double cursorWidth; + /// {@template flutter.widgets.editableText.cursorRadius} /// How rounded the corners of the cursor should be. /// - /// By default, the cursor has a Radius of zero. + /// By default, the cursor has no radius. + /// {@endtemplate} final Radius cursorRadius; /// The appearance of the keyboard. @@ -392,6 +432,7 @@ /// Defaults to [Brightness.light]. final Brightness keyboardAppearance; + /// {@template flutter.widgets.editableText.scrollPadding} /// Configures padding to edges surrounding a [Scrollable] when the Textfield scrolls into view. /// /// When this widget receives focus and is not completely visible (for example scrolled partially @@ -400,6 +441,7 @@ /// This value controls how far from the edges of a [Scrollable] the TextField will be positioned after the scroll. /// /// Defaults to EdgeInserts.all(20.0). + /// {@endtemplate} final EdgeInsets scrollPadding; /// {@template flutter.widgets.editableText.enableInteractiveSelection}
diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index 7e2a85f..42a61cc 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart
@@ -531,6 +531,9 @@ onPanUpdate: _handleDragUpdate, onTap: _handleTap, child: Stack( + // Always let the selection handles draw outside of the conceptual + // box where (0,0) is the top left corner of the RenderEditable. + overflow: Overflow.visible, children: <Widget>[ Positioned( left: point.dx,
diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart new file mode 100644 index 0000000..ddfff50 --- /dev/null +++ b/packages/flutter/test/cupertino/text_field_test.dart
@@ -0,0 +1,688 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class MockClipboard { + Object _clipboardData = <String, dynamic>{ + 'text': null, + }; + + Future<dynamic> handleMethodCall(MethodCall methodCall) async { + switch (methodCall.method) { + case 'Clipboard.getData': + return _clipboardData; + case 'Clipboard.setData': + _clipboardData = methodCall.arguments; + break; + } + } +} + +void main() { + final MockClipboard mockClipboard = MockClipboard(); + SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall); + + testWidgets( + 'takes available space horizontally and takes intrinsic space vertically', + (WidgetTester tester) async { + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: ConstrainedBox( + constraints: BoxConstraints.loose(const Size(200, 200)), + child: const CupertinoTextField(), + ), + ), + ), + ); + + expect( + tester.getSize(find.byType(CupertinoTextField)), + const Size(200, 29), // 29 is the height of the default font + padding etc. + ); + }, + ); + + testWidgets( + 'multi-lined text fields are intrinsically taller', + (WidgetTester tester) async { + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: ConstrainedBox( + constraints: BoxConstraints.loose(const Size(200, 200)), + child: const CupertinoTextField(maxLines: 3), + ), + ), + ), + ); + + expect( + tester.getSize(find.byType(CupertinoTextField)), + const Size(200, 63), + ); + }, + ); + + testWidgets( + 'default text field has a border', + (WidgetTester tester) async { + await tester.pumpWidget( + const CupertinoApp( + home: Center( + child: CupertinoTextField(), + ), + ), + ); + + final BoxDecoration decoration = tester.widget<DecoratedBox>( + find.descendant( + of: find.byType(CupertinoTextField), + matching: find.byType(DecoratedBox) + ), + ).decoration; + + expect( + decoration.borderRadius, + BorderRadius.circular(4.0), + ); + expect( + decoration.border.bottom.color, + CupertinoColors.lightBackgroundGray, + ); + }, + ); + + testWidgets( + 'decoration can be overrriden', + (WidgetTester tester) async { + await tester.pumpWidget( + const CupertinoApp( + home: Center( + child: CupertinoTextField( + decoration: null, + ), + ), + ), + ); + + expect( + find.descendant( + of: find.byType(CupertinoTextField), + matching: find.byType(DecoratedBox) + ), + findsNothing, + ); + }, + ); + + testWidgets( + 'text entries are padded by default', + (WidgetTester tester) async { + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + controller: TextEditingController(text: 'initial'), + ), + ), + ), + ); + + expect( + tester.getTopLeft(find.text('initial')) - tester.getTopLeft(find.byType(CupertinoTextField)), + const Offset(6.0, 6.0), + ); + }, + ); + + testWidgets( + 'can control text content via controller', + (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(); + + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + controller: controller, + ), + ), + ), + ); + + controller.text = 'controller text'; + await tester.pump(); + + expect(find.text('controller text'), findsOneWidget); + + controller.text = ''; + await tester.pump(); + + expect(find.text('controller text'), findsNothing); + }, + ); + + testWidgets( + 'placeholders are lightly colored and disappears once typing starts', + (WidgetTester tester) async { + await tester.pumpWidget( + const CupertinoApp( + home: Center( + child: CupertinoTextField( + placeholder: 'placeholder', + ), + ), + ), + ); + + final Text placeholder = tester.widget(find.text('placeholder')); + expect(placeholder.style.color, const Color(0xFFC2C2C2)); + + await tester.enterText(find.byType(CupertinoTextField), 'input'); + await tester.pump(); + expect(find.text('placeholder'), findsNothing); + }, + ); + + testWidgets( + 'prefix widget is in front of the text', + (WidgetTester tester) async { + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + prefix: const Icon(CupertinoIcons.add), + controller: TextEditingController(text: 'input'), + ), + ), + ), + ); + + expect( + tester.getTopRight(find.byIcon(CupertinoIcons.add)).dx + 6.0, // 6px standard padding around input. + tester.getTopLeft(find.byType(EditableText)).dx, + ); + + expect( + tester.getTopLeft(find.byType(EditableText)).dx, + tester.getTopLeft(find.byType(CupertinoTextField)).dx + + tester.getSize(find.byIcon(CupertinoIcons.add)).width + + 6.0, + ); + }, + ); + + testWidgets( + 'prefix widget respects visibility mode', + (WidgetTester tester) async { + await tester.pumpWidget( + const CupertinoApp( + home: Center( + child: CupertinoTextField( + prefix: Icon(CupertinoIcons.add), + prefixMode: OverlayVisibilityMode.editing, + ), + ), + ), + ); + + expect(find.byIcon(CupertinoIcons.add), findsNothing); + // The position should just be the edge of the whole text field plus padding. + expect( + tester.getTopLeft(find.byType(EditableText)).dx, + tester.getTopLeft(find.byType(CupertinoTextField)).dx + 6.0, + ); + + await tester.enterText(find.byType(CupertinoTextField), 'text input'); + await tester.pump(); + + expect(find.text('text input'), findsOneWidget); + expect(find.byIcon(CupertinoIcons.add), findsOneWidget); + + // Text is now moved to the right. + expect( + tester.getTopLeft(find.byType(EditableText)).dx, + tester.getTopLeft(find.byType(CupertinoTextField)).dx + + tester.getSize(find.byIcon(CupertinoIcons.add)).width + + 6.0, + ); + }, + ); + + testWidgets( + 'suffix widget is after the text', + (WidgetTester tester) async { + await tester.pumpWidget( + const CupertinoApp( + home: Center( + child: CupertinoTextField( + suffix: Icon(CupertinoIcons.add), + ), + ), + ), + ); + + expect( + tester.getTopRight(find.byType(EditableText)).dx + 6.0, + tester.getTopLeft(find.byIcon(CupertinoIcons.add)).dx, // 6px standard padding around input. + ); + + expect( + tester.getTopRight(find.byType(EditableText)).dx, + tester.getTopRight(find.byType(CupertinoTextField)).dx + - tester.getSize(find.byIcon(CupertinoIcons.add)).width + - 6.0, + ); + }, + ); + + testWidgets( + 'suffix widget respects visibility mode', + (WidgetTester tester) async { + await tester.pumpWidget( + const CupertinoApp( + home: Center( + child: CupertinoTextField( + suffix: Icon(CupertinoIcons.add), + suffixMode: OverlayVisibilityMode.notEditing, + ), + ), + ), + ); + + expect(find.byIcon(CupertinoIcons.add), findsOneWidget); + + await tester.enterText(find.byType(CupertinoTextField), 'text input'); + await tester.pump(); + + expect(find.text('text input'), findsOneWidget); + expect(find.byIcon(CupertinoIcons.add), findsNothing); + }, + ); + + testWidgets( + 'can customize padding', + (WidgetTester tester) async { + await tester.pumpWidget( + const CupertinoApp( + home: Center( + child: CupertinoTextField( + padding: EdgeInsets.zero, + ), + ), + ), + ); + + expect( + tester.getSize(find.byType(EditableText)), + tester.getSize(find.byType(CupertinoTextField)), + ); + }, + ); + + testWidgets( + 'padding is in between prefix and suffix', + (WidgetTester tester) async { + await tester.pumpWidget( + const CupertinoApp( + home: Center( + child: CupertinoTextField( + padding: EdgeInsets.all(20.0), + prefix: SizedBox(height: 100.0, width: 100.0), + suffix: SizedBox(height: 50.0, width: 50.0), + ), + ), + ), + ); + + expect( + tester.getTopLeft(find.byType(EditableText)).dx, + // Size of prefix + padding. + 100.0 + 20.0, + ); + + expect(tester.getTopLeft(find.byType(EditableText)).dy, 291.5); + + expect( + tester.getTopRight(find.byType(EditableText)).dx, + 800.0 - 50.0 - 20.0, + ); + + await tester.pumpWidget( + const CupertinoApp( + home: Center( + child: CupertinoTextField( + padding: EdgeInsets.all(30.0), + prefix: SizedBox(height: 100.0, width: 100.0), + suffix: SizedBox(height: 50.0, width: 50.0), + ), + ), + ), + ); + + expect( + tester.getTopLeft(find.byType(EditableText)).dx, + 100.0 + 30.0, + ); + + // Since the highest component, the prefix box, is higher than + // the text + paddings, the text's vertical position isn't affected. + expect(tester.getTopLeft(find.byType(EditableText)).dy, 291.5); + + expect( + tester.getTopRight(find.byType(EditableText)).dx, + 800.0 - 50.0 - 30.0, + ); + }, + ); + + testWidgets( + 'clear button shows with right visibility mode', + (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(); + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + controller: controller, + placeholder: 'placeholder does not affect clear button', + clearButtonMode: OverlayVisibilityMode.always, + ), + ), + ), + ); + + expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsOneWidget); + + expect( + tester.getTopRight(find.byType(EditableText)).dx, + 800.0 - 30.0 /* size of button */ - 6.0 /* padding */, + ); + + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + controller: controller, + placeholder: 'placeholder does not affect clear button', + clearButtonMode: OverlayVisibilityMode.editing, + ), + ), + ), + ); + + expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsNothing); + expect( + tester.getTopRight(find.byType(EditableText)).dx, + 800.0 - 6.0 /* padding */, + ); + + await tester.enterText(find.byType(CupertinoTextField), 'text input'); + await tester.pump(); + + expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsOneWidget); + expect(find.text('text input'), findsOneWidget); + expect( + tester.getTopRight(find.byType(EditableText)).dx, + 800.0 - 30.0 - 6.0, + ); + + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + controller: controller, + placeholder: 'placeholder does not affect clear button', + clearButtonMode: OverlayVisibilityMode.notEditing, + ), + ), + ), + ); + expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsNothing); + + controller.text = ''; + await tester.pump(); + + expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsOneWidget); + }, + ); + + testWidgets( + 'clear button removes text', + (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(); + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + controller: controller, + placeholder: 'placeholder', + clearButtonMode: OverlayVisibilityMode.editing, + ), + ), + ), + ); + + controller.text = 'text entry'; + await tester.pump(); + + await tester.tap(find.byIcon(CupertinoIcons.clear_thick_circled)); + await tester.pump(); + + expect(controller.text, ''); + expect(find.text('placeholder'), findsOneWidget); + expect(find.text('text entry'), findsNothing); + expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsNothing); + }, + ); + + testWidgets( + 'clear button yields precedence to suffix', + (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(); + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + controller: controller, + clearButtonMode: OverlayVisibilityMode.always, + suffix: const Icon(CupertinoIcons.add_circled_solid), + suffixMode: OverlayVisibilityMode.editing, + ), + ), + ), + ); + + expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsOneWidget); + expect(find.byIcon(CupertinoIcons.add_circled_solid), findsNothing); + + expect( + tester.getTopRight(find.byType(EditableText)).dx, + 800.0 - 30.0 /* size of button */ - 6.0 /* padding */, + ); + + controller.text = 'non empty text'; + await tester.pump(); + + expect(find.byIcon(CupertinoIcons.clear_thick_circled), findsNothing); + expect(find.byIcon(CupertinoIcons.add_circled_solid), findsOneWidget); + + // Still just takes the space of one widget. + expect( + tester.getTopRight(find.byType(EditableText)).dx, + 800.0 - 24.0 /* size of button */ - 6.0 /* padding */, + ); + }, + ); + + testWidgets( + 'font style controls intrinsic height', + (WidgetTester tester) async { + await tester.pumpWidget( + const CupertinoApp( + home: Center( + child: CupertinoTextField(), + ), + ), + ); + + expect( + tester.getSize(find.byType(CupertinoTextField)).height, + 29.0, + ); + + await tester.pumpWidget( + const CupertinoApp( + home: Center( + child: CupertinoTextField( + style: TextStyle( + // A larger font. + fontSize: 50.0, + ), + ), + ), + ), + ); + + expect( + tester.getSize(find.byType(CupertinoTextField)).height, + 62.0, + ); + }, + ); + + testWidgets( + 'RTL puts attachments to the right places', + (WidgetTester tester) async { + await tester.pumpWidget( + const CupertinoApp( + home: Directionality( + textDirection: TextDirection.rtl, + child: Center( + child: CupertinoTextField( + padding: EdgeInsets.all(20.0), + prefix: Icon(CupertinoIcons.book), + clearButtonMode: OverlayVisibilityMode.always, + ), + ), + ), + ), + ); + + expect( + tester.getTopLeft(find.byIcon(CupertinoIcons.book)).dx, + 800.0 - 24.0, + ); + + expect( + tester.getTopRight(find.byIcon(CupertinoIcons.clear_thick_circled)).dx, + 24.0, + ); + }, + ); + + testWidgets( + 'text fields with no max lines can grow', + (WidgetTester tester) async { + await tester.pumpWidget( + const CupertinoApp( + home: Center( + child: CupertinoTextField( + maxLines: null, + ), + ), + ), + ); + + expect( + tester.getSize(find.byType(CupertinoTextField)).height, + 29.0, // Initially one line high. + ); + + await tester.enterText(find.byType(CupertinoTextField), '\n'); + await tester.pump(); + + expect( + tester.getSize(find.byType(CupertinoTextField)).height, + 46.0, // Initially one line high. + ); + }, + ); + + testWidgets('cannot enter new lines onto single line TextField', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(); + + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField( + controller: controller, + ), + ), + ), + ); + + await tester.enterText(find.byType(CupertinoTextField), 'abc\ndef'); + + expect(controller.text, 'abcdef'); + }); + + testWidgets('copy paste', (WidgetTester tester) async { + await tester.pumpWidget( + CupertinoApp( + home: Column( + children: const <Widget>[ + CupertinoTextField( + placeholder: 'field 1', + ), + CupertinoTextField( + placeholder: 'field 2', + ), + ], + ), + ), + ); + + await tester.enterText( + find.widgetWithText(CupertinoTextField, 'field 1'), + "j'aime la poutine" + ); + await tester.pump(); + + // Tap an area inside the EditableText but with no text. + await tester.longPressAt( + tester.getTopRight(find.text("j'aime la poutine")) + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + + await tester.tap(find.text('Select All')); + await tester.pump(); + + await tester.tap(find.text('Cut')); + await tester.pump(); + + // Placeholder 1 is back since the text is cut. + expect(find.text('field 1'), findsOneWidget); + expect(find.text('field 2'), findsOneWidget); + + await tester.longPress(find.text('field 2')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + + await tester.tap(find.text('Paste')); + await tester.pump(); + + expect(find.text('field 1'), findsOneWidget); + expect(find.text("j'aime la poutine"), findsOneWidget); + expect(find.text('field 2'), findsNothing); + }); +}