Unverified Commit b63ced55 authored by xster's avatar xster Committed by GitHub

Add a CupertinoTextField (#23194)

parent dc36195c
......@@ -11,3 +11,4 @@ export 'cupertino_refresh_demo.dart';
export 'cupertino_segmented_control_demo.dart';
export 'cupertino_slider_demo.dart';
export 'cupertino_switch_demo.dart';
export 'cupertino_text_field_demo.dart';
// 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(),
),
],
),
),
);
}
}
......@@ -500,6 +500,13 @@ List<GalleryDemo> _buildGalleryDemos() {
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(
......
......@@ -28,6 +28,7 @@ export 'src/cupertino/slider.dart';
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';
......@@ -243,7 +243,7 @@ class _CupertinoAppState extends State<CupertinoApp> {
_navigatorObservers = List<NavigatorObserver>.from(widget.navigatorObservers)
..add(_heroController);
} else {
_navigatorObservers = null;
_navigatorObservers = const <NavigatorObserver>[];
}
}
......
......@@ -721,4 +721,44 @@ class CupertinoIcons {
/// * [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);
}
// 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),
),
),
),
),
);
}
}
......@@ -20,14 +20,16 @@ const double _kToolbarHeight = 36.0;
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 @@ class _TextSelectionToolbar extends StatelessWidget {
// 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)),
],
);
}
......
......@@ -154,10 +154,7 @@ class TextField extends StatefulWidget {
/// 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 @@ class TextField extends StatefulWidget {
/// [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 @@ class TextField extends StatefulWidget {
/// 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 @@ class TextField extends StatefulWidget {
/// [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 @@ class TextField extends StatefulWidget {
/// [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 @@ class TextField extends StatefulWidget {
/// 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}
......
......@@ -242,22 +242,31 @@ class EditableText extends StatefulWidget {
/// 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 @@ class EditableText extends StatefulWidget {
/// 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 @@ class EditableText extends StatefulWidget {
/// 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 @@ class EditableText extends StatefulWidget {
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 @@ class EditableText extends StatefulWidget {
///
/// 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.
///
/// 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.
/// 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 @@ class EditableText extends StatefulWidget {
/// 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 @@ class EditableText extends StatefulWidget {
/// 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 @@ class EditableText extends StatefulWidget {
/// 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 @@ class EditableText extends StatefulWidget {
/// 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 @@ class EditableText extends StatefulWidget {
/// 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}
......
......@@ -531,6 +531,9 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay
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,
......
// 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);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment