Unverified Commit 4d4d017a authored by Daniel Edrisian's avatar Daniel Edrisian Committed by GitHub

Re-land CupertinoFormSection, CupertinoFormRow, and CupertinoTextFormFieldRow (#71522)

parent 94d574fa
...@@ -25,6 +25,8 @@ export 'src/cupertino/context_menu.dart'; ...@@ -25,6 +25,8 @@ export 'src/cupertino/context_menu.dart';
export 'src/cupertino/context_menu_action.dart'; export 'src/cupertino/context_menu_action.dart';
export 'src/cupertino/date_picker.dart'; export 'src/cupertino/date_picker.dart';
export 'src/cupertino/dialog.dart'; export 'src/cupertino/dialog.dart';
export 'src/cupertino/form_row.dart';
export 'src/cupertino/form_section.dart';
export 'src/cupertino/icon_theme_data.dart'; export 'src/cupertino/icon_theme_data.dart';
export 'src/cupertino/icons.dart'; export 'src/cupertino/icons.dart';
export 'src/cupertino/interface_level.dart'; export 'src/cupertino/interface_level.dart';
...@@ -43,6 +45,7 @@ export 'src/cupertino/switch.dart'; ...@@ -43,6 +45,7 @@ export 'src/cupertino/switch.dart';
export 'src/cupertino/tab_scaffold.dart'; export 'src/cupertino/tab_scaffold.dart';
export 'src/cupertino/tab_view.dart'; export 'src/cupertino/tab_view.dart';
export 'src/cupertino/text_field.dart'; export 'src/cupertino/text_field.dart';
export 'src/cupertino/text_form_field_row.dart';
export 'src/cupertino/text_selection.dart'; export 'src/cupertino/text_selection.dart';
export 'src/cupertino/text_theme.dart'; export 'src/cupertino/text_theme.dart';
export 'src/cupertino/theme.dart'; export 'src/cupertino/theme.dart';
......
// Copyright 2014 The Flutter 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/widgets.dart';
import 'colors.dart';
import 'theme.dart';
// Content padding determined via SwiftUI's `Form` view in the iOS 14.2 SDK.
const EdgeInsetsGeometry _kDefaultPadding =
EdgeInsetsDirectional.fromSTEB(16.0, 6.0, 6.0, 6.0);
/// An iOS-style form row.
///
/// Creates an iOS-style split form row with a standard prefix and child widget.
/// Also provides a space for error and helper widgets that appear underneath.
///
/// The [child] parameter is required. This widget is displayed at the end of
/// the row.
///
/// The [prefix] parameter is optional and is displayed at the start of the
/// row. Standard iOS guidelines encourage passing a [Text] widget to [prefix]
/// to detail the nature of the row's [child] widget.
///
/// The [padding] parameter is used to pad the contents of the row. It defaults
/// to the standard iOS padding. If no edge insets are intended, explicitly pass
/// [EdgeInsets.zero] to [padding].
///
/// The [helper] and [error] parameters are both optional widgets targeted at
/// displaying more information about the row. Both widgets are placed
/// underneath the [prefix] and [child], and will expand the row's height to
/// accomodate for their presence. When a [Text] is given to [error], it will
/// be shown in [CupertinoColors.destructiveRed] coloring and
/// medium-weighted font.
///
/// {@tool snippet}
///
/// Creates a [CupertinoFormSection] containing a [CupertinoFormRow] with the
/// [prefix], [child], [helper] and [error] widgets.
///
/// ```dart
/// class FlutterDemo extends StatefulWidget {
/// FlutterDemo({Key key}) : super(key: key);
///
/// @override
/// _FlutterDemoState createState() => _FlutterDemoState();
/// }
///
/// class _FlutterDemoState extends State<FlutterDemo> {
/// bool toggleValue = false;
///
/// @override
/// Widget build(BuildContext context) {
/// return CupertinoPageScaffold(
/// child: Center(
/// child: CupertinoFormSection(
/// header: Text('SECTION 1'),
/// children: <Widget>[
/// CupertinoFormRow(
/// child: CupertinoSwitch(
/// value: this.toggleValue,
/// onChanged: (value) {
/// setState(() {
/// this.toggleValue = value;
/// });
/// },
/// ),
/// prefix: Text('Toggle'),
/// helper: Text('Use your instincts'),
/// error: toggleValue ? Text('Cannot be true') : null,
/// ),
/// ],
/// ),
/// ),
/// );
/// }
/// }
/// ```
/// {@end-tool}
class CupertinoFormRow extends StatelessWidget {
/// Creates an iOS-style split form row with a standard prefix and child widget.
/// Also provides a space for error and helper widgets that appear underneath.
///
/// The [child] parameter is required. This widget is displayed at the end of
/// the row.
///
/// The [prefix] parameter is optional and is displayed at the start of the
/// row. Standard iOS guidelines encourage passing a [Text] widget to [prefix]
/// to detail the nature of the row's [child] widget.
///
/// The [padding] parameter is used to pad the contents of the row. It defaults
/// to the standard iOS padding. If no edge insets are intended, explicitly
/// pass [EdgeInsets.zero] to [padding].
///
/// The [helper] and [error] parameters are both optional widgets targeted at
/// displaying more information about the row. Both widgets are placed
/// underneath the [prefix] and [child], and will expand the row's height to
/// accomodate for their presence. When a [Text] is given to [error], it will
/// be shown in [CupertinoColors.destructiveRed] coloring and
/// medium-weighted font.
const CupertinoFormRow({
Key? key,
required this.child,
this.prefix,
this.padding,
this.helper,
this.error,
}) : super(key: key);
/// A widget that is displayed at the start of the row.
///
/// The [prefix] parameter is displayed at the start of the row. Standard iOS
/// guidelines encourage passing a [Text] widget to [prefix] to detail the
/// nature of the row's [child] widget. If null, the [child] widget will take
/// up all horizontal space in the row.
final Widget? prefix;
/// Content padding for the row.
///
/// Defaults to the standard iOS padding for form rows. If no edge insets are
/// intended, explicitly pass [EdgeInsets.zero] to [padding].
final EdgeInsetsGeometry? padding;
/// A widget that is displayed underneath the [prefix] and [child] widgets.
///
/// The [helper] appears in primary label coloring, and is meant to inform the
/// user about interaction with the child widget. The row becomes taller in
/// order to display the [helper] widget underneath [prefix] and [child]. If
/// null, the row is shorter.
final Widget? helper;
/// A widget that is displayed underneath the [prefix] and [child] widgets.
///
/// The [error] widget is primarily used to inform users of input errors. When
/// a [Text] is given to [error], it will be shown in
/// [CupertinoColors.destructiveRed] coloring and medium-weighted font. The
/// row becomes taller in order to display the [helper] widget underneath
/// [prefix] and [child]. If null, the row is shorter.
final Widget? error;
/// Child widget.
///
/// The [child] widget is primarily used for input. It end-aligned and
/// horizontally flexible, taking up the entire space trailing past the
/// [prefix] widget.
final Widget child;
@override
Widget build(BuildContext context) {
final CupertinoThemeData themeData = CupertinoTheme.of(context);
final TextStyle textStyle = themeData.textTheme.textStyle;
final List<Widget> rowChildren = <Widget>[
if (prefix != null)
DefaultTextStyle(
style: textStyle,
child: prefix!,
),
Flexible(
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: child,
),
),
];
return Padding(
padding: padding ?? _kDefaultPadding,
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: rowChildren,
),
if (helper != null)
Align(
alignment: AlignmentDirectional.centerStart,
child: DefaultTextStyle(
style: textStyle,
child: helper!,
),
),
if (error != null)
Align(
alignment: AlignmentDirectional.centerStart,
child: DefaultTextStyle(
style: const TextStyle(
color: CupertinoColors.destructiveRed,
fontWeight: FontWeight.w500,
),
child: error!,
),
),
],
),
);
}
}
// Copyright 2014 The Flutter 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/widgets.dart';
import 'colors.dart';
// Standard header margin, determined from SwiftUI's Forms in iOS 14.2 SDK.
const EdgeInsetsDirectional _kDefaultHeaderMargin =
EdgeInsetsDirectional.fromSTEB(16.5, 16.0, 16.5, 10.0);
// Used for iOS "Inset Grouped" margin, determined from SwiftUI's Forms in
// iOS 14.2 SDK.
const EdgeInsetsDirectional _kDefaultInsetGroupedRowsMargin =
EdgeInsetsDirectional.fromSTEB(16.5, 0.0, 16.5, 16.5);
// Used for iOS "Inset Grouped" border radius, estimated from SwiftUI's Forms in
// iOS 14.2 SDK.
// TODO(edrisian): This should be a rounded rectangle once that shape is added.
const BorderRadius _kDefaultInsetGroupedBorderRadius =
BorderRadius.all(Radius.circular(10.0));
// Used to differentiate the edge-to-edge section with the centered section.
enum _CupertinoFormSectionType { base, insetGrouped }
/// An iOS-style form section.
///
/// The base constructor for [CupertinoFormSection] constructs an
/// edge-to-edge style section which includes an iOS-style header, rows,
/// the dividers between rows, and borders on top and bottom of the rows.
///
/// The [CupertinoFormSection.insetGrouped] constructor creates a round-edged and
/// padded section that is commonly seen in notched-displays like iPhone X and
/// beyond. Creates an iOS-style header, rows, and the dividers
/// between rows. Does not create borders on top and bottom of the rows.
///
/// The [header] parameter sets the form section header. The section header lies
/// above the [children] rows, with margins that match the iOS style.
///
/// The [children] parameter is required and sets the list of rows shown in
/// the section. The [children] parameter takes a list, as opposed to a more
/// efficient builder function that lazy builds, because forms are intended to
/// be short in row count. It is recommended that only [CupertinoFormRow] and
/// [CupertinoTextFormFieldRow] widgets be included in the [children] list in
/// order to retain the iOS look.
///
/// The [margin] parameter sets the spacing around the content area of the
/// section encapsulating [children].
///
/// The [decoration] parameter sets the decoration around [children].
/// If null, defaults to [CupertinoColors.secondarySystemGroupedBackground].
/// If null, defaults to 10.0 circular radius when constructing with
/// [CupertinoFormSection.insetGrouped]. Defaults to zero radius for the
/// standard [CupertinoFormSection] constructor.
///
/// The [backgroundColor] parameter sets the background color behind the section.
/// If null, defaults to [CupertinoColors.systemGroupedBackground].
///
/// {@macro flutter.material.Material.clipBehavior}
class CupertinoFormSection extends StatelessWidget {
/// Creates a section that mimicks standard iOS forms.
///
/// The base constructor for [CupertinoFormSection] constructs an
/// edge-to-edge style section which includes an iOS-style header,
/// rows, the dividers between rows, and borders on top and bottom of the rows.
///
/// The [header] parameter sets the form section header. The section header
/// lies above the [children] rows, with margins that match the iOS style.
///
/// The [children] parameter is required and sets the list of rows shown in
/// the section. The [children] parameter takes a list, as opposed to a more
/// efficient builder function that lazy builds, because forms are intended to
/// be short in row count. It is recommended that only [CupertinoFormRow] and
/// [CupertinoTextFormFieldRow] widgets be included in the [children] list in
/// order to retain the iOS look.
///
/// The [margin] parameter sets the spacing around the content area of the
/// section encapsulating [children], and defaults to zero padding.
///
/// The [decoration] parameter sets the decoration around [children].
/// If null, defaults to [CupertinoColors.secondarySystemGroupedBackground].
/// If null, defaults to 10.0 circular radius when constructing with
/// [CupertinoFormSection.insetGrouped]. Defaults to zero radius for the
/// standard [CupertinoFormSection] constructor.
///
/// The [backgroundColor] parameter sets the background color behind the
/// section. If null, defaults to [CupertinoColors.systemGroupedBackground].
///
/// {@macro flutter.material.Material.clipBehavior}
const CupertinoFormSection({
Key? key,
required this.children,
this.header,
this.margin = EdgeInsets.zero,
this.backgroundColor = CupertinoColors.systemGroupedBackground,
this.decoration,
this.clipBehavior = Clip.none,
}) : _type = _CupertinoFormSectionType.base,
assert(children.length > 0),
super(key: key);
/// Creates a section that mimicks standard "Inset Grouped" iOS forms.
///
/// The [CupertinoFormSection.insetGrouped] constructor creates a round-edged and
/// padded section that is commonly seen in notched-displays like iPhone X and
/// beyond. Creates an iOS-style header, rows, and the dividers
/// between rows. Does not create borders on top and bottom of the rows.
///
/// The [header] parameter sets the form section header. The section header
/// lies above the [children] rows, with margins that match the iOS style.
///
/// The [children] parameter is required and sets the list of rows shown in
/// the section. The [children] parameter takes a list, as opposed to a more
/// efficient builder function that lazy builds, because forms are intended to
/// be short in row count. It is recommended that only [CupertinoFormRow] and
/// [CupertinoTextFormFieldRow] widgets be included in the [children] list in
/// order to retain the iOS look.
///
/// The [margin] parameter sets the spacing around the content area of the
/// section encapsulating [children], and defaults to the standard
/// notched-style iOS form padding.
///
/// The [decoration] parameter sets the decoration around [children].
/// If null, defaults to [CupertinoColors.secondarySystemGroupedBackground].
/// If null, defaults to 10.0 circular radius when constructing with
/// [CupertinoFormSection.insetGrouped]. Defaults to zero radius for the
/// standard [CupertinoFormSection] constructor.
///
/// The [backgroundColor] parameter sets the background color behind the
/// section. If null, defaults to [CupertinoColors.systemGroupedBackground].
///
/// {@macro flutter.material.Material.clipBehavior}
const CupertinoFormSection.insetGrouped({
Key? key,
required this.children,
this.header,
this.margin = _kDefaultInsetGroupedRowsMargin,
this.backgroundColor = CupertinoColors.systemGroupedBackground,
this.decoration,
this.clipBehavior = Clip.none,
}) : _type = _CupertinoFormSectionType.insetGrouped,
assert(children.length > 0),
super(key: key);
final _CupertinoFormSectionType _type;
/// Sets the form section header. The section header lies above the
/// [children] rows.
final Widget? header;
/// Margin around the content area of the section encapsulating [children].
///
/// Defaults to zero padding if constructed with standard
/// [CupertinoFormSection] constructor. Defaults to the standard notched-style
/// iOS margin when constructing with [CupertinoFormSection.insetGrouped].
final EdgeInsetsGeometry margin;
/// The list of rows in the section.
///
/// This takes a list, as opposed to a more efficient builder function that
/// lazy builds, because forms are intended to be short in row count. It is
/// recommended that only [CupertinoFormRow] and [CupertinoTextFormFieldRow]
/// widgets be included in the [children] list in order to retain the iOS look.
final List<Widget> children;
/// Sets the decoration around [children].
///
/// If null, background color defaults to
/// [CupertinoColors.secondarySystemGroupedBackground].
///
/// If null, border radius defaults to 10.0 circular radius when constructing
/// with [CupertinoFormSection.insetGrouped]. Defaults to zero radius for the
/// standard [CupertinoFormSection] constructor.
final BoxDecoration? decoration;
/// Sets the background color behind the section.
///
/// Defaults to [CupertinoColors.systemGroupedBackground].
final Color backgroundColor;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.none], and must not be null.
final Clip clipBehavior;
@override
Widget build(BuildContext context) {
final Color dividerColor = CupertinoColors.separator.resolveFrom(context);
final double dividerHeight = 1.0 / MediaQuery.of(context).devicePixelRatio;
// Long divider is used for wrapping the top and bottom of rows.
// Only used in _CupertinoFormSectionType.base mode
final Widget longDivider = Container(
color: dividerColor,
height: dividerHeight,
);
// Short divider is used between rows.
// The value of the starting inset (15.0) is determined using SwiftUI's Form
// seperators in the iOS 14.2 SDK.
final Widget shortDivider = Container(
margin: const EdgeInsetsDirectional.only(start: 15.0),
color: dividerColor,
height: dividerHeight,
);
// We construct childrenWithDividers as follows:
// Insert a short divider between all rows.
// If it is a `_CupertinoFormSectionType.base` type, add a long divider
// to the top and bottom of the rows.
assert(children.isNotEmpty);
final List<Widget> childrenWithDividers = <Widget>[];
if (_type == _CupertinoFormSectionType.base) {
childrenWithDividers.add(longDivider);
}
children.sublist(0, children.length - 1).forEach((Widget widget) {
childrenWithDividers.add(widget);
childrenWithDividers.add(shortDivider);
});
childrenWithDividers.add(children.last);
if (_type == _CupertinoFormSectionType.base) {
childrenWithDividers.add(longDivider);
}
// Refactored the decorate children group in one place to avoid repeating it
// twice down bellow in the returned widget.
final DecoratedBox decoratedChildrenGroup = DecoratedBox(
decoration: decoration ??
BoxDecoration(
color: CupertinoDynamicColor.resolve(
decoration?.color ??
CupertinoColors.secondarySystemGroupedBackground,
context),
borderRadius: _kDefaultInsetGroupedBorderRadius,
),
child: Column(
children: childrenWithDividers,
),
);
return DecoratedBox(
decoration: BoxDecoration(
color: CupertinoDynamicColor.resolve(backgroundColor, context),
),
child: Column(
children: <Widget>[
Align(
alignment: AlignmentDirectional.centerStart,
child: header == null
? null
: DefaultTextStyle(
style: TextStyle(
fontSize: 13.5,
color:
CupertinoColors.secondaryLabel.resolveFrom(context),
),
child: Padding(
padding: _kDefaultHeaderMargin,
child: header!,
),
),
),
Padding(
padding: margin,
child: clipBehavior == Clip.none
? decoratedChildrenGroup
: ClipRRect(
borderRadius: _kDefaultInsetGroupedBorderRadius,
clipBehavior: clipBehavior,
child: decoratedChildrenGroup),
),
],
),
);
}
}
...@@ -16,6 +16,11 @@ import 'theme.dart'; ...@@ -16,6 +16,11 @@ import 'theme.dart';
export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization, SmartQuotesType, SmartDashesType; export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization, SmartQuotesType, SmartDashesType;
const TextStyle _kDefaultPlaceholderStyle = TextStyle(
fontWeight: FontWeight.w400,
color: CupertinoColors.placeholderText,
);
// Value inspected from Xcode 11 & iOS 13.0 Simulator. // Value inspected from Xcode 11 & iOS 13.0 Simulator.
const BorderSide _kDefaultRoundedBorderSide = BorderSide( const BorderSide _kDefaultRoundedBorderSide = BorderSide(
color: CupertinoDynamicColor.withBrightness( color: CupertinoDynamicColor.withBrightness(
...@@ -326,6 +331,148 @@ class CupertinoTextField extends StatefulWidget { ...@@ -326,6 +331,148 @@ class CupertinoTextField extends StatefulWidget {
)), )),
super(key: key); super(key: key);
/// Creates a borderless 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.
///
/// The text cursor is not shown if [showCursor] is false or if [showCursor]
/// is null (the default) and [readOnly] is true.
///
/// If specified, the [maxLength] property must be greater than zero.
///
/// The [selectionHeightStyle] and [selectionWidthStyle] properties allow
/// changing the shape of the selection highlighting. These properties default
/// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively and
/// must not be null.
///
/// The [autocorrect], [autofocus], [clearButtonMode], [dragStartBehavior],
/// [expands], [maxLengthEnforced], [obscureText], [prefixMode], [readOnly],
/// [scrollPadding], [suffixMode], [textAlign], [selectionHeightStyle],
/// [selectionWidthStyle], and [enableSuggestions] properties must not be null.
///
/// See also:
///
/// * [minLines], which is the minimum number of lines to occupy when the
/// content spans fewer lines.
/// * [expands], to allow the widget to size itself to its parent's height.
/// * [maxLength], which discusses the precise meaning of "number of
/// characters" and how it may differ from the intuitive meaning.
const CupertinoTextField.borderless({
Key? key,
this.controller,
this.focusNode,
this.decoration,
this.padding = const EdgeInsets.all(6.0),
this.placeholder,
this.placeholderStyle = _kDefaultPlaceholderStyle,
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,
this.strutStyle,
this.textAlign = TextAlign.start,
this.textAlignVertical,
this.readOnly = false,
ToolbarOptions? toolbarOptions,
this.showCursor,
this.autofocus = false,
this.obscuringCharacter = '•',
this.obscureText = false,
this.autocorrect = true,
SmartDashesType? smartDashesType,
SmartQuotesType? smartQuotesType,
this.enableSuggestions = true,
this.maxLines = 1,
this.minLines,
this.expands = false,
this.maxLength,
this.maxLengthEnforced = true,
this.onChanged,
this.onEditingComplete,
this.onSubmitted,
this.inputFormatters,
this.enabled,
this.cursorWidth = 2.0,
this.cursorHeight,
this.cursorRadius = const Radius.circular(2.0),
this.cursorColor,
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
this.keyboardAppearance,
this.scrollPadding = const EdgeInsets.all(20.0),
this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true,
this.selectionControls,
this.onTap,
this.scrollController,
this.scrollPhysics,
this.autofillHints,
this.restorationId,
}) : assert(textAlign != null),
assert(readOnly != null),
assert(autofocus != null),
// TODO(a14n): uncomment when issue is fixed, https://github.com/dart-lang/sdk/issues/43407
assert(obscuringCharacter != null/* && obscuringCharacter.length == 1*/),
assert(obscureText != null),
assert(autocorrect != null),
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
assert(enableSuggestions != null),
assert(maxLengthEnforced != null),
assert(scrollPadding != null),
assert(dragStartBehavior != null),
assert(selectionHeightStyle != null),
assert(selectionWidthStyle != null),
assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
assert(expands != null),
assert(
!expands || (maxLines == null && minLines == null),
'minLines and maxLines must be null when expands is true.',
),
assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
assert(maxLength == null || maxLength > 0),
assert(clearButtonMode != null),
assert(prefixMode != null),
assert(suffixMode != null),
// Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set.
assert(!identical(textInputAction, TextInputAction.newline) ||
maxLines == 1 ||
!identical(keyboardType, TextInputType.text),
'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.'),
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
toolbarOptions = toolbarOptions ?? (obscureText ?
const ToolbarOptions(
selectAll: true,
paste: true,
) :
const ToolbarOptions(
copy: true,
cut: true,
selectAll: true,
paste: true,
)),
super(key: key);
/// Controls the text being edited. /// Controls the text being edited.
/// ///
/// If null, this widget will create its own [TextEditingController]. /// If null, this widget will create its own [TextEditingController].
......
// Copyright 2014 The Flutter 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 'form_row.dart';
import 'text_field.dart';
import 'theme.dart';
/// Creates a [CupertinoFormRow] containing a [FormField] that wraps
/// a [CupertinoTextField].
///
/// A [Form] ancestor is not required. The [Form] simply makes it easier to
/// save, reset, or validate multiple fields at once. To use without a [Form],
/// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to
/// save or reset the form field.
///
/// When a [controller] is specified, its [TextEditingController.text]
/// defines the [initialValue]. If this [FormField] is part of a scrolling
/// container that lazily constructs its children, like a [ListView] or a
/// [CustomScrollView], then a [controller] should be specified.
/// The controller's lifetime should be managed by a stateful widget ancestor
/// of the scrolling container.
///
/// The [prefix] parameter is displayed at the start of the row. Standard iOS
/// guidelines encourage passing a [Text] widget to [prefix] to detail the
/// nature of the input.
///
/// The [padding] parameter is used to pad the contents of the row. It is
/// directly passed to [CupertinoFormRow]. If the [padding]
/// parameter is null, [CupertinoFormRow] constructs its own default
/// padding (which is the standard form row padding in iOS.) If no edge
/// insets are intended, explicitly pass [EdgeInsets.zero] to [padding].
///
/// If a [controller] is not specified, [initialValue] can be used to give
/// the automatically generated controller an initial value.
///
/// Consider calling [TextEditingController.dispose] of the [controller], if one
/// is specified, when it is no longer needed. This will ensure we discard any
/// resources used by the object.
///
/// For documentation about the various parameters, see the
/// [CupertinoTextField] class and [new CupertinoTextField.borderless],
/// the constructor.
///
/// {@tool snippet}
///
/// Creates a [CupertinoTextFormFieldRow] with a leading text and validator
/// function.
///
/// If the user enters valid text, the CupertinoTextField appears normally
/// without any warnings to the user.
///
/// If the user enters invalid text, the error message returned from the
/// validator function is displayed in dark red underneath the input.
///
/// ```dart
/// CupertinoTextFormFieldRow(
/// prefix: Text('Username'),
/// onSaved: (String value) {
/// // This optional block of code can be used to run
/// // code when the user saves the form.
/// },
/// validator: (String value) {
/// return value.contains('@') ? 'Do not use the @ char.' : null;
/// },
/// )
/// ```
/// {@end-tool}
///
/// {@tool dartpad --template=stateful_widget_material}
/// This example shows how to move the focus to the next field when the user
/// presses the SPACE key.
///
/// ```dart imports
/// import 'package:flutter/cupertino.dart';
/// ```
///
/// ```dart
/// Widget build(BuildContext context) {
/// return CupertinoPageScaffold(
/// child: Center(
/// child: Form(
/// autovalidateMode: AutovalidateMode.always,
/// onChanged: () {
/// Form.of(primaryFocus.context).save();
/// },
/// child: CupertinoFormSection.insetGrouped(
/// header: Text('SECTION 1'),
/// children: List<Widget>.generate(5, (int index) {
/// return CupertinoTextFormFieldRow(
/// prefix: Text('Enter text'),
/// placeholder: 'Enter text',
/// validator: (value) {
/// if (value.isEmpty) {
/// return 'Please enter a value';
/// }
/// return null;
/// },
/// );
/// }),
/// ),
/// ),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
class CupertinoTextFormFieldRow extends FormField<String> {
/// Creates a [CupertinoFormRow] containing a [FormField] that wraps
/// a [CupertinoTextField].
///
/// When a [controller] is specified, [initialValue] must be null (the
/// default). If [controller] is null, then a [TextEditingController]
/// will be constructed automatically and its `text` will be initialized
/// to [initialValue] or the empty string.
///
/// The [prefix] parameter is displayed at the start of the row. Standard iOS
/// guidelines encourage passing a [Text] widget to [prefix] to detail the
/// nature of the input.
///
/// The [padding] parameter is used to pad the contents of the row. It is
/// directly passed to [CupertinoFormRow]. If the [padding]
/// parameter is null, [CupertinoFormRow] constructs its own default
/// padding (which is the standard form row padding in iOS.) If no edge
/// insets are intended, explicitly pass [EdgeInsets.zero] to [padding].
///
/// For documentation about the various parameters, see the
/// [CupertinoTextField] class and [new CupertinoTextField.borderless],
/// the constructor.
CupertinoTextFormFieldRow({
Key? key,
this.prefix,
this.padding,
this.controller,
String? initialValue,
FocusNode? focusNode,
BoxDecoration? decoration,
TextInputType? keyboardType,
TextCapitalization textCapitalization = TextCapitalization.none,
TextInputAction? textInputAction,
TextStyle? style,
StrutStyle? strutStyle,
TextAlign textAlign = TextAlign.start,
TextAlignVertical? textAlignVertical,
bool autofocus = false,
bool readOnly = false,
ToolbarOptions? toolbarOptions,
bool? showCursor,
String obscuringCharacter = '•',
bool obscureText = false,
bool autocorrect = true,
SmartDashesType? smartDashesType,
SmartQuotesType? smartQuotesType,
bool enableSuggestions = true,
int? maxLines = 1,
int? minLines,
bool expands = false,
int? maxLength,
ValueChanged<String>? onChanged,
GestureTapCallback? onTap,
VoidCallback? onEditingComplete,
ValueChanged<String>? onFieldSubmitted,
FormFieldSetter<String>? onSaved,
FormFieldValidator<String>? validator,
List<TextInputFormatter>? inputFormatters,
bool? enabled,
double cursorWidth = 2.0,
double? cursorHeight,
Color? cursorColor,
Brightness? keyboardAppearance,
EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
bool enableInteractiveSelection = true,
TextSelectionControls? selectionControls,
ScrollPhysics? scrollPhysics,
Iterable<String>? autofillHints,
AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
String? placeholder,
TextStyle? placeholderStyle = const TextStyle(
fontWeight: FontWeight.w400,
color: CupertinoColors.placeholderText,
),
}) : assert(initialValue == null || controller == null),
assert(textAlign != null),
assert(autofocus != null),
assert(readOnly != null),
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
assert(obscureText != null),
assert(autocorrect != null),
assert(enableSuggestions != null),
assert(scrollPadding != null),
assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
assert(expands != null),
assert(
!expands || (maxLines == null && minLines == null),
'minLines and maxLines must be null when expands is true.',
),
assert(!obscureText || maxLines == 1,
'Obscured fields cannot be multiline.'),
assert(maxLength == null || maxLength > 0),
assert(enableInteractiveSelection != null),
super(
key: key,
initialValue: controller?.text ?? initialValue ?? '',
onSaved: onSaved,
validator: validator,
autovalidateMode: autovalidateMode,
builder: (FormFieldState<String> field) {
final _CupertinoTextFormFieldRowState state =
field as _CupertinoTextFormFieldRowState;
void onChangedHandler(String value) {
field.didChange(value);
if (onChanged != null) {
onChanged(value);
}
}
return CupertinoFormRow(
prefix: prefix,
padding: padding,
error: (field.errorText == null) ? null : Text(field.errorText!),
child: CupertinoTextField.borderless(
controller: state._effectiveController,
focusNode: focusNode,
keyboardType: keyboardType,
decoration: decoration,
textInputAction: textInputAction,
style: style,
strutStyle: strutStyle,
textAlign: textAlign,
textAlignVertical: textAlignVertical,
textCapitalization: textCapitalization,
autofocus: autofocus,
toolbarOptions: toolbarOptions,
readOnly: readOnly,
showCursor: showCursor,
obscuringCharacter: obscuringCharacter,
obscureText: obscureText,
autocorrect: autocorrect,
smartDashesType: smartDashesType,
smartQuotesType: smartQuotesType,
enableSuggestions: enableSuggestions,
maxLines: maxLines,
minLines: minLines,
expands: expands,
maxLength: maxLength,
onChanged: onChangedHandler,
onTap: onTap,
onEditingComplete: onEditingComplete,
onSubmitted: onFieldSubmitted,
inputFormatters: inputFormatters,
enabled: enabled,
cursorWidth: cursorWidth,
cursorHeight: cursorHeight,
cursorColor: cursorColor,
scrollPadding: scrollPadding,
scrollPhysics: scrollPhysics,
keyboardAppearance: keyboardAppearance,
enableInteractiveSelection: enableInteractiveSelection,
selectionControls: selectionControls,
autofillHints: autofillHints,
placeholder: placeholder,
placeholderStyle: placeholderStyle,
),
);
},
);
/// A widget that is displayed at the start of the row.
///
/// The [prefix] widget is displayed at the start of the row. Standard iOS
/// guidelines encourage passing a [Text] widget to [prefix] to detail the
/// nature of the input.
final Widget? prefix;
/// Content padding for the row.
///
/// The [padding] widget is passed to [CupertinoFormRow]. If the [padding]
/// parameter is null, [CupertinoFormRow] constructs its own default
/// padding, which is the standard form row padding in iOS.
///
/// If no edge insets are intended, explicitly pass [EdgeInsets.zero] to
/// [padding].
final EdgeInsetsGeometry? padding;
/// Controls the text being edited.
///
/// If null, this widget will create its own [TextEditingController] and
/// initialize its [TextEditingController.text] with [initialValue].
final TextEditingController? controller;
@override
_CupertinoTextFormFieldRowState createState() =>
_CupertinoTextFormFieldRowState();
}
class _CupertinoTextFormFieldRowState extends FormFieldState<String> {
TextEditingController? _controller;
TextEditingController? get _effectiveController =>
widget.controller ?? _controller;
@override
CupertinoTextFormFieldRow get widget =>
super.widget as CupertinoTextFormFieldRow;
@override
void initState() {
super.initState();
if (widget.controller == null) {
_controller = TextEditingController(text: widget.initialValue);
} else {
widget.controller!.addListener(_handleControllerChanged);
}
}
@override
void didUpdateWidget(CupertinoTextFormFieldRow oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
oldWidget.controller?.removeListener(_handleControllerChanged);
widget.controller?.addListener(_handleControllerChanged);
if (oldWidget.controller != null && widget.controller == null) {
_controller =
TextEditingController.fromValue(oldWidget.controller!.value);
}
if (widget.controller != null) {
setValue(widget.controller!.text);
if (oldWidget.controller == null) {
_controller = null;
}
}
}
}
@override
void dispose() {
widget.controller?.removeListener(_handleControllerChanged);
super.dispose();
}
@override
void didChange(String? value) {
super.didChange(value);
if (value != null && _effectiveController!.text != value) {
_effectiveController!.text = value;
}
}
@override
void reset() {
super.reset();
if (widget.initialValue != null) {
setState(() {
_effectiveController!.text = widget.initialValue!;
});
}
}
void _handleControllerChanged() {
// Suppress changes that originated from within this class.
//
// In the case where a controller has been passed in to this widget, we
// register this change listener. In these cases, we'll also receive change
// notifications for changes originating from within this class -- for
// example, the reset() method. In such cases, the FormField value will
// already have been set.
if (_effectiveController!.text != value) {
didChange(_effectiveController!.text);
}
}
}
// Copyright 2014 The Flutter 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';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
void main() {
testWidgets('Shows prefix', (WidgetTester tester) async {
const Widget prefix = Text('Enter Value');
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoFormRow(
prefix: prefix,
child: CupertinoTextField(),
),
),
),
);
expect(prefix, tester.widget(find.byType(Text)));
});
testWidgets('Shows child', (WidgetTester tester) async {
const Widget child = CupertinoTextField();
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoFormRow(
child: child,
),
),
),
);
expect(child, tester.widget(find.byType(CupertinoTextField)));
});
testWidgets('RTL puts prefix after child', (WidgetTester tester) async {
const Widget prefix = Text('Enter Value');
const Widget child = CupertinoTextField();
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: Directionality(
textDirection: TextDirection.rtl,
child: CupertinoFormRow(
prefix: prefix,
child: child,
),
),
),
),
);
expect(
tester.getTopLeft(find.byType(Text)).dx >
tester.getTopLeft(find.byType(CupertinoTextField)).dx,
true);
});
testWidgets('LTR puts child after prefix', (WidgetTester tester) async {
const Widget prefix = Text('Enter Value');
const Widget child = CupertinoTextField();
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: Directionality(
textDirection: TextDirection.ltr,
child: CupertinoFormRow(
prefix: prefix,
child: child,
),
),
),
),
);
expect(
tester.getTopLeft(find.byType(Text)).dx >
tester.getTopLeft(find.byType(CupertinoTextField)).dx,
false);
});
testWidgets('Shows error widget', (WidgetTester tester) async {
const Widget error = Text('Error');
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoFormRow(
child: CupertinoTextField(),
error: error,
),
),
),
);
expect(error, tester.widget(find.byType(Text)));
});
testWidgets('Shows helper widget', (WidgetTester tester) async {
const Widget helper = Text('Helper');
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoFormRow(
child: CupertinoTextField(),
helper: helper,
),
),
),
);
expect(helper, tester.widget(find.byType(Text)));
});
testWidgets('Shows helper text above error text',
(WidgetTester tester) async {
const Widget helper = Text('Helper');
const Widget error = CupertinoActivityIndicator();
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoFormRow(
child: CupertinoTextField(),
helper: helper,
error: error,
),
),
),
);
expect(
tester.getTopLeft(find.byType(CupertinoActivityIndicator)).dy >
tester.getTopLeft(find.byType(Text)).dy,
true);
});
testWidgets('Shows helper in label color and error text in red color',
(WidgetTester tester) async {
const Widget helper = Text('Helper');
const Widget error = Text('Error');
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: CupertinoFormRow(
child: CupertinoTextField(),
helper: helper,
error: error,
),
),
),
);
final DefaultTextStyle helperTextStyle =
tester.widget(find.byType(DefaultTextStyle).first);
expect(helperTextStyle.style.color, CupertinoColors.label);
final DefaultTextStyle errorTextStyle =
tester.widget(find.byType(DefaultTextStyle).last);
expect(errorTextStyle.style.color, CupertinoColors.destructiveRed);
});
}
// Copyright 2014 The Flutter 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';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
void main() {
testWidgets('Shows header', (WidgetTester tester) async {
const Widget header = Text('Enter Value');
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoFormSection(
header: header,
children: <Widget>[CupertinoTextFormFieldRow()],
),
),
),
);
expect(header, tester.widget(find.byType(Text)));
});
testWidgets('Shows long dividers in edge-to-edge section part 1',
(WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoFormSection(
children: <Widget>[CupertinoTextFormFieldRow()],
),
),
),
);
// Since the children list is reconstructed with dividers in it, the column
// retrieved should have 3 items for an input [children] param with 1 child.
final Column childrenColumn = tester.widget(find.byType(Column).at(1));
expect(childrenColumn.children.length, 3);
});
testWidgets('Shows long dividers in edge-to-edge section part 2',
(WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoFormSection(
children: <Widget>[
CupertinoTextFormFieldRow(),
CupertinoTextFormFieldRow()
],
),
),
),
);
// Since the children list is reconstructed with dividers in it, the column
// retrieved should have 5 items for an input [children] param with 2
// children. Two long dividers, two rows, and one short divider.
final Column childrenColumn = tester.widget(find.byType(Column).at(1));
expect(childrenColumn.children.length, 5);
});
testWidgets('Does not show long dividers in insetGrouped section part 1',
(WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoFormSection.insetGrouped(
children: <Widget>[CupertinoTextFormFieldRow()],
),
),
),
);
// Since the children list is reconstructed without long dividers in it, the
// column retrieved should have 1 item for an input [children] param with 1
// child.
final Column childrenColumn = tester.widget(find.byType(Column).at(1));
expect(childrenColumn.children.length, 1);
});
testWidgets('Does not show long dividers in insetGrouped section part 2',
(WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
restorationScopeId: 'App',
home: Center(
child: CupertinoFormSection.insetGrouped(
children: <Widget>[
CupertinoTextFormFieldRow(),
CupertinoTextFormFieldRow()
],
),
),
),
);
// Since the children list is reconstructed with short dividers in it, the
// column retrieved should have 3 items for an input [children] param with 2
// children. Two long dividers, two rows, and one short divider.
final Column childrenColumn = tester.widget(find.byType(Column).at(1));
expect(childrenColumn.children.length, 3);
});
testWidgets('Sets background color for section', (WidgetTester tester) async {
const Color backgroundColor = CupertinoColors.systemBlue;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: CupertinoFormSection(
children: <Widget>[CupertinoTextFormFieldRow()],
backgroundColor: backgroundColor,
),
),
),
);
final DecoratedBox decoratedBox =
tester.widget(find.byType(DecoratedBox).first);
final BoxDecoration boxDecoration =
decoratedBox.decoration as BoxDecoration;
expect(boxDecoration.color, backgroundColor);
});
testWidgets('Setting clipBehavior clips children section',
(WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoFormSection(
children: <Widget>[CupertinoTextFormFieldRow()],
clipBehavior: Clip.antiAlias,
),
),
),
);
expect(find.byType(ClipRRect), findsOneWidget);
});
testWidgets('Not setting clipBehavior does not clip children section',
(WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoFormSection(
children: <Widget>[CupertinoTextFormFieldRow()],
),
),
),
);
expect(find.byType(ClipRRect), findsNothing);
});
}
// Copyright 2014 The Flutter 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';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import '../rendering/mock_canvas.dart';
void main() {
testWidgets('Passes textAlign to underlying CupertinoTextField',
(WidgetTester tester) async {
const TextAlign alignment = TextAlign.center;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
textAlign: alignment,
),
),
),
);
final Finder textFieldFinder = find.byType(CupertinoTextField);
expect(textFieldFinder, findsOneWidget);
final CupertinoTextField textFieldWidget = tester.widget(textFieldFinder);
expect(textFieldWidget.textAlign, alignment);
});
testWidgets('Passes scrollPhysics to underlying TextField',
(WidgetTester tester) async {
const ScrollPhysics scrollPhysics = ScrollPhysics();
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
scrollPhysics: scrollPhysics,
),
),
),
);
final Finder textFieldFinder = find.byType(CupertinoTextField);
expect(textFieldFinder, findsOneWidget);
final CupertinoTextField textFieldWidget = tester.widget(textFieldFinder);
expect(textFieldWidget.scrollPhysics, scrollPhysics);
});
testWidgets('Passes textAlignVertical to underlying CupertinoTextField',
(WidgetTester tester) async {
const TextAlignVertical textAlignVertical = TextAlignVertical.bottom;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
textAlignVertical: textAlignVertical,
),
),
),
);
final Finder textFieldFinder = find.byType(CupertinoTextField);
expect(textFieldFinder, findsOneWidget);
final CupertinoTextField textFieldWidget = tester.widget(textFieldFinder);
expect(textFieldWidget.textAlignVertical, textAlignVertical);
});
testWidgets('Passes textInputAction to underlying CupertinoTextField',
(WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
textInputAction: TextInputAction.next,
),
),
),
);
final Finder textFieldFinder = find.byType(CupertinoTextField);
expect(textFieldFinder, findsOneWidget);
final CupertinoTextField textFieldWidget = tester.widget(textFieldFinder);
expect(textFieldWidget.textInputAction, TextInputAction.next);
});
testWidgets('Passes onEditingComplete to underlying CupertinoTextField',
(WidgetTester tester) async {
final VoidCallback onEditingComplete = () {};
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
onEditingComplete: onEditingComplete,
),
),
),
);
final Finder textFieldFinder = find.byType(CupertinoTextField);
expect(textFieldFinder, findsOneWidget);
final CupertinoTextField textFieldWidget = tester.widget(textFieldFinder);
expect(textFieldWidget.onEditingComplete, onEditingComplete);
});
testWidgets('Passes cursor attributes to underlying CupertinoTextField',
(WidgetTester tester) async {
const double cursorWidth = 3.14;
const double cursorHeight = 6.28;
const Radius cursorRadius = Radius.circular(2);
const Color cursorColor = CupertinoColors.systemPurple;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
cursorWidth: cursorWidth,
cursorHeight: cursorHeight,
cursorColor: cursorColor,
),
),
),
);
final Finder textFieldFinder = find.byType(CupertinoTextField);
expect(textFieldFinder, findsOneWidget);
final CupertinoTextField textFieldWidget = tester.widget(textFieldFinder);
expect(textFieldWidget.cursorWidth, cursorWidth);
expect(textFieldWidget.cursorHeight, cursorHeight);
expect(textFieldWidget.cursorRadius, cursorRadius);
expect(textFieldWidget.cursorColor, cursorColor);
});
testWidgets('onFieldSubmit callbacks are called',
(WidgetTester tester) async {
bool _called = false;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
onFieldSubmitted: (String value) {
_called = true;
},
),
),
),
);
await tester.showKeyboard(find.byType(CupertinoTextField));
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump();
expect(_called, true);
});
testWidgets('onChanged callbacks are called', (WidgetTester tester) async {
late String _value;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
onChanged: (String value) {
_value = value;
},
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), 'Soup');
await tester.pump();
expect(_value, 'Soup');
});
testWidgets('autovalidateMode is passed to super',
(WidgetTester tester) async {
int _validateCalled = 0;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
autovalidateMode: AutovalidateMode.always,
validator: (String? value) {
_validateCalled++;
return null;
},
),
),
),
);
expect(_validateCalled, 1);
await tester.enterText(find.byType(CupertinoTextField), 'a');
await tester.pump();
expect(_validateCalled, 2);
});
testWidgets('validate is called if widget is enabled',
(WidgetTester tester) async {
int _validateCalled = 0;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
enabled: true,
autovalidateMode: AutovalidateMode.always,
validator: (String? value) {
_validateCalled += 1;
return null;
},
),
),
),
);
expect(_validateCalled, 1);
await tester.enterText(find.byType(CupertinoTextField), 'a');
await tester.pump();
expect(_validateCalled, 2);
});
testWidgets('readonly text form field will hide cursor by default',
(WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
initialValue: 'readonly',
readOnly: true,
),
),
),
);
await tester.showKeyboard(find.byType(CupertinoTextFormFieldRow));
expect(tester.testTextInput.hasAnyClients, false);
await tester.tap(find.byType(CupertinoTextField));
await tester.pump();
expect(tester.testTextInput.hasAnyClients, false);
await tester.longPress(find.byType(CupertinoTextFormFieldRow));
await tester.pump();
// Context menu should not have paste.
expect(find.text('Paste'), findsNothing);
final EditableTextState editableTextState =
tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
// Make sure it does not paint caret for a period of time.
await tester.pump(const Duration(milliseconds: 200));
expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0));
await tester.pump(const Duration(milliseconds: 200));
expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0));
await tester.pump(const Duration(milliseconds: 200));
expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0));
}, skip: isBrowser); // We do not use Flutter-rendered context menu on the Web
testWidgets('onTap is called upon tap', (WidgetTester tester) async {
int tapCount = 0;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
onTap: () {
tapCount += 1;
},
),
),
),
);
expect(tapCount, 0);
await tester.tap(find.byType(CupertinoTextField));
// Wait a bit so they're all single taps and not double taps.
await tester.pump(const Duration(milliseconds: 300));
await tester.tap(find.byType(CupertinoTextField));
await tester.pump(const Duration(milliseconds: 300));
await tester.tap(find.byType(CupertinoTextField));
await tester.pump(const Duration(milliseconds: 300));
expect(tapCount, 3);
});
// Regression test for https://github.com/flutter/flutter/issues/54472.
testWidgets('reset resets the text fields value to the initialValue',
(WidgetTester tester) async {
await tester.pumpWidget(CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
initialValue: 'initialValue',
),
),
));
await tester.enterText(
find.byType(CupertinoTextFormFieldRow), 'changedValue');
final FormFieldState<String> state = tester
.state<FormFieldState<String>>(find.byType(CupertinoTextFormFieldRow));
state.reset();
expect(find.text('changedValue'), findsNothing);
expect(find.text('initialValue'), findsOneWidget);
});
// Regression test for https://github.com/flutter/flutter/issues/54472.
testWidgets('didChange changes text fields value',
(WidgetTester tester) async {
await tester.pumpWidget(CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
initialValue: 'initialValue',
),
),
));
expect(find.text('initialValue'), findsOneWidget);
final FormFieldState<String> state = tester
.state<FormFieldState<String>>(find.byType(CupertinoTextFormFieldRow));
state.didChange('changedValue');
expect(find.text('initialValue'), findsNothing);
expect(find.text('changedValue'), findsOneWidget);
});
testWidgets('onChanged callbacks value and FormFieldState.value are sync',
(WidgetTester tester) async {
bool _called = false;
late FormFieldState<String> state;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
onChanged: (String value) {
_called = true;
expect(value, state.value);
},
),
),
),
);
state = tester
.state<FormFieldState<String>>(find.byType(CupertinoTextFormFieldRow));
await tester.enterText(find.byType(CupertinoTextField), 'Soup');
expect(_called, true);
});
testWidgets('autofillHints is passed to super', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
autofillHints: const <String>[AutofillHints.countryName],
),
),
),
);
final CupertinoTextField widget =
tester.widget(find.byType(CupertinoTextField));
expect(widget.autofillHints,
equals(const <String>[AutofillHints.countryName]));
});
testWidgets('autovalidateMode is passed to super',
(WidgetTester tester) async {
int _validateCalled = 0;
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CupertinoTextFormFieldRow(
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (String? value) {
_validateCalled++;
return null;
},
),
),
),
);
expect(_validateCalled, 0);
await tester.enterText(find.byType(CupertinoTextField), 'a');
await tester.pump();
expect(_validateCalled, 1);
});
testWidgets('AutovalidateMode.always mode shows error from the start',
(WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
initialValue: 'Value',
autovalidateMode: AutovalidateMode.always,
validator: (String? value) => 'Error',
),
),
),
);
final Finder errorTextFinder = find.byType(Text);
expect(errorTextFinder, findsOneWidget);
final Text errorText = tester.widget(errorTextFinder);
expect(errorText.data, 'Error');
});
testWidgets('Shows error text upon invalid input',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: '');
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
controller: controller,
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (String? value) => 'Error',
),
),
),
);
expect(find.byType(Text), findsNothing);
controller.text = 'Value';
await tester.pumpAndSettle();
final Finder errorTextFinder = find.byType(Text);
expect(errorTextFinder, findsOneWidget);
final Text errorText = tester.widget(errorTextFinder);
expect(errorText.data, 'Error');
});
testWidgets('Shows prefix', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextFormFieldRow(
prefix: const Text('Enter Value'),
),
),
),
);
final Finder errorTextFinder = find.byType(Text);
expect(errorTextFinder, findsOneWidget);
final Text errorText = tester.widget(errorTextFinder);
expect(errorText.data, 'Enter Value');
});
}
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