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);
+  });
+}