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!,
),
),
],
),
);
}
}
This diff is collapsed.
...@@ -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].
......
This diff is collapsed.
// 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);
});
}
This diff is collapsed.
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