From 5cfb16b193886e68e51cc7256389d78a21f31bfd Mon Sep 17 00:00:00 2001 From: Christopher Fujino Date: Fri, 19 Jun 2020 12:03:38 -0700 Subject: [PATCH] Re-land "Deprecate WhitelistingTextInputFormatter and BlacklistingTextInputFormatter (#59120)" (#59876) This relands #59120, which was reverted in #59870. --- analysis_options.yaml | 3 + dev/bots/analyze.dart | 11 +- .../root/packages/foo/deprecation.dart | 38 ++- dev/bots/test/analyze_test.dart | 7 +- .../demo/material/text_form_field_demo.dart | 2 +- .../flutter/lib/src/cupertino/nav_bar.dart | 6 +- packages/flutter/lib/src/material/dialog.dart | 5 - .../lib/src/material/input_decorator.dart | 20 +- .../pickers/calendar_date_range_picker.dart | 6 +- .../pickers/date_picker_deprecated.dart | 6 +- .../material/pickers/date_picker_header.dart | 2 +- .../lib/src/material/pickers/date_utils.dart | 3 +- .../material/pickers/input_date_picker.dart | 12 +- .../pickers/input_date_range_picker.dart | 6 +- .../lib/src/material/pickers/pickers.dart | 6 + .../flutter/lib/src/material/scaffold.dart | 1 - .../flutter/lib/src/material/text_theme.dart | 4 - .../lib/src/services/asset_bundle.dart | 2 +- .../lib/src/services/platform_channel.dart | 6 +- .../lib/src/services/text_formatter.dart | 273 +++++++++++++----- packages/flutter/lib/src/widgets/basic.dart | 6 +- .../lib/src/widgets/editable_text.dart | 2 +- .../test/material/input_decorator_test.dart | 3 - .../test/material/text_field_test.dart | 109 ++++++- .../test/services/text_formatter_test.dart | 144 +++++++++ .../widgets/listener_deprecated_test.dart | 4 - .../test/widgets/text_formatter_test.dart | 111 ++++++- .../lib/src/extension/extension.dart | 12 +- .../test/src/real_tests/extension_test.dart | 10 +- 29 files changed, 662 insertions(+), 158 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 213bf942ac900..9990dcbd07e55 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -29,6 +29,9 @@ analyzer: missing_return: warning # allow having TODOs in the code todo: ignore + # allow self-reference to deprecated members (we do this because otherwise we have + # to annotate every member in every test, assert, etc, when we deprecate something) + deprecated_member_use_from_same_package: ignore # Ignore analyzer hints for updating pubspecs when using Future or # Stream and not importing dart:async # Please see https://github.com/flutter/flutter/pull/24528 for details. diff --git a/dev/bots/analyze.dart b/dev/bots/analyze.dart index b0e93534deaff..d4c788b4259a2 100644 --- a/dev/bots/analyze.dart +++ b/dev/bots/analyze.dart @@ -135,7 +135,7 @@ Future run(List arguments) async { final RegExp _findDeprecationPattern = RegExp(r'@[Dd]eprecated'); final RegExp _deprecationPattern1 = RegExp(r'^( *)@Deprecated\($'); // ignore: flutter_deprecation_syntax (see analyze.dart) final RegExp _deprecationPattern2 = RegExp(r"^ *'(.+) '$"); -final RegExp _deprecationPattern3 = RegExp(r"^ *'This feature was deprecated after v([0-9]+)\.([0-9]+)\.([0-9]+)\.'$"); +final RegExp _deprecationPattern3 = RegExp(r"^ *'This feature was deprecated after v([0-9]+)\.([0-9]+)\.([0-9]+)(\-[0-9]+\.[0-9]+\.pre)?\.'$"); final RegExp _deprecationPattern4 = RegExp(r'^ *\)$'); /// Some deprecation notices are special, for example they're used to annotate members that @@ -182,7 +182,7 @@ Future verifyDeprecations(String workingDirectory, { int minimumMatches = if (message == null) { final String firstChar = String.fromCharCode(match2[1].runes.first); if (firstChar.toUpperCase() != firstChar) - throw 'Deprecation notice should be a grammatically correct sentence and start with a capital letter; see style guide.'; + throw 'Deprecation notice should be a grammatically correct sentence and start with a capital letter; see style guide: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo'; } message = match2[1]; lineNumber += 1; @@ -190,6 +190,13 @@ Future verifyDeprecations(String workingDirectory, { int minimumMatches = throw 'Incomplete deprecation notice.'; match3 = _deprecationPattern3.firstMatch(lines[lineNumber]); } while (match3 == null); + final int v1 = int.parse(match3[1]); + final int v2 = int.parse(match3[2]); + final bool hasV4 = match3[4] != null; + if (v1 > 1 || (v1 == 1 && v2 >= 20)) { + if (!hasV4) + throw 'Deprecation notice does not accurately indicate a dev branch version number; please see https://flutter.dev/docs/development/tools/sdk/releases to find the latest dev build version number.'; + } if (!message.endsWith('.') && !message.endsWith('!') && !message.endsWith('?')) throw 'Deprecation notice should be a grammatically correct sentence and end with a period.'; if (!lines[lineNumber].startsWith("$indent '")) diff --git a/dev/bots/test/analyze-test-input/root/packages/foo/deprecation.dart b/dev/bots/test/analyze-test-input/root/packages/foo/deprecation.dart index aad90fbaa0694..b430a0a3693d1 100644 --- a/dev/bots/test/analyze-test-input/root/packages/foo/deprecation.dart +++ b/dev/bots/test/analyze-test-input/root/packages/foo/deprecation.dart @@ -55,6 +55,42 @@ void test10() { } @Deprecated( 'URLs are not required. ' - 'This feature was deprecated after v2.0.0.' + 'This feature was deprecated after v1.0.0.' ) void test11() { } + +@Deprecated( + 'Version number test (should fail). ' + 'This feature was deprecated after v1.19.0.' +) +void test12() { } + +@Deprecated( + 'Version number test (should fail). ' + 'This feature was deprecated after v1.20.0.' +) +void test13() { } + +@Deprecated( + 'Version number test (should fail). ' + 'This feature was deprecated after v1.21.0.' +) +void test14() { } + +@Deprecated( + 'Version number test (should fail). ' + 'This feature was deprecated after v3.1.0.' +) +void test15() { } + +@Deprecated( + 'Version number test (should be fine). ' + 'This feature was deprecated after v0.1.0.' +) +void test16() { } + +@Deprecated( + 'Version number test (should be fine). ' + 'This feature was deprecated after v1.20.0-1.0.pre.' +) +void test17() { } diff --git a/dev/bots/test/analyze_test.dart b/dev/bots/test/analyze_test.dart index faba5bb0503cf..d3799b39db6b8 100644 --- a/dev/bots/test/analyze_test.dart +++ b/dev/bots/test/analyze_test.dart @@ -41,7 +41,7 @@ void main() { + ( 'test/analyze-test-input/root/packages/foo/deprecation.dart:12: Deprecation notice does not match required pattern.\n' - 'test/analyze-test-input/root/packages/foo/deprecation.dart:18: Deprecation notice should be a grammatically correct sentence and start with a capital letter; see style guide.\n' + 'test/analyze-test-input/root/packages/foo/deprecation.dart:18: Deprecation notice should be a grammatically correct sentence and start with a capital letter; see style guide: STYLE_GUIDE_URL\n' 'test/analyze-test-input/root/packages/foo/deprecation.dart:25: Deprecation notice should be a grammatically correct sentence and end with a period.\n' 'test/analyze-test-input/root/packages/foo/deprecation.dart:29: Deprecation notice does not match required pattern.\n' 'test/analyze-test-input/root/packages/foo/deprecation.dart:32: Deprecation notice does not match required pattern.\n' @@ -49,7 +49,12 @@ void main() { 'test/analyze-test-input/root/packages/foo/deprecation.dart:41: Deprecation notice does not match required pattern.\n' 'test/analyze-test-input/root/packages/foo/deprecation.dart:48: End of deprecation notice does not match required pattern.\n' 'test/analyze-test-input/root/packages/foo/deprecation.dart:51: Unexpected deprecation notice indent.\n' + 'test/analyze-test-input/root/packages/foo/deprecation.dart:70: Deprecation notice does not accurately indicate a dev branch version number; please see RELEASES_URL to find the latest dev build version number.\n' + 'test/analyze-test-input/root/packages/foo/deprecation.dart:76: Deprecation notice does not accurately indicate a dev branch version number; please see RELEASES_URL to find the latest dev build version number.\n' + 'test/analyze-test-input/root/packages/foo/deprecation.dart:82: Deprecation notice does not accurately indicate a dev branch version number; please see RELEASES_URL to find the latest dev build version number.\n' .replaceAll('/', Platform.isWindows ? r'\' : '/') + .replaceAll('STYLE_GUIDE_URL', 'https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo') + .replaceAll('RELEASES_URL', 'https://flutter.dev/docs/development/tools/sdk/releases') ) + 'See: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes\n' diff --git a/dev/integration_tests/flutter_gallery/lib/demo/material/text_form_field_demo.dart b/dev/integration_tests/flutter_gallery/lib/demo/material/text_form_field_demo.dart index cd87f74534fe8..1dbeccaa4aa8e 100644 --- a/dev/integration_tests/flutter_gallery/lib/demo/material/text_form_field_demo.dart +++ b/dev/integration_tests/flutter_gallery/lib/demo/material/text_form_field_demo.dart @@ -217,7 +217,7 @@ class TextFormFieldDemoState extends State { validator: _validatePhoneNumber, // TextInputFormatters are applied in sequence. inputFormatters: [ - WhitelistingTextInputFormatter.digitsOnly, + FilteringTextInputFormatter.digitsOnly, // Fit the validating format. _phoneNumberFormatter, ], diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart index b6c5a4ea756c1..2e98b17c6b332 100644 --- a/packages/flutter/lib/src/cupertino/nav_bar.dart +++ b/packages/flutter/lib/src/cupertino/nav_bar.dart @@ -463,7 +463,7 @@ class _CupertinoNavigationBarState extends State { ); final Color actionsForegroundColor = CupertinoDynamicColor.resolve( - widget.actionsForegroundColor, // ignore: deprecated_member_use_from_same_package + widget.actionsForegroundColor, context, ); if (!widget.transitionBetweenRoutes || !_isTransitionable(context)) { @@ -694,8 +694,8 @@ class _CupertinoSliverNavigationBarState extends State with TickerProviderStat final bool labelIsInitiallyFloating = widget.decoration.floatingLabelBehavior == FloatingLabelBehavior.always || (widget.decoration.floatingLabelBehavior != FloatingLabelBehavior.never && - // ignore: deprecated_member_use_from_same_package widget.decoration.hasFloatingPlaceholder && widget._labelShouldWithdraw); @@ -1976,7 +1975,6 @@ class _InputDecoratorState extends State with TickerProviderStat bool get isHovering => widget.isHovering && decoration.enabled; bool get isEmpty => widget.isEmpty; bool get _floatingLabelEnabled { - // ignore: deprecated_member_use_from_same_package return decoration.hasFloatingPlaceholder && decoration.floatingLabelBehavior != FloatingLabelBehavior.never; } @@ -1987,7 +1985,6 @@ class _InputDecoratorState extends State with TickerProviderStat _effectiveDecoration = null; final bool floatBehaviorChanged = widget.decoration.floatingLabelBehavior != old.decoration.floatingLabelBehavior - // ignore: deprecated_member_use_from_same_package || widget.decoration.hasFloatingPlaceholder != old.decoration.hasFloatingPlaceholder; if (widget._labelShouldWithdraw != old._labelShouldWithdraw || floatBehaviorChanged) { @@ -2520,7 +2517,7 @@ class InputDecoration { 'Use floatingLabelBehavior instead. ' 'This feature was deprecated after v1.13.2.' ) - this.hasFloatingPlaceholder = true, // ignore: deprecated_member_use_from_same_package + this.hasFloatingPlaceholder = true, this.floatingLabelBehavior = FloatingLabelBehavior.auto, this.isCollapsed = false, this.isDense, @@ -2566,7 +2563,6 @@ class InputDecoration { 'Use floatingLabelBehavior instead. ' 'This feature was deprecated after v1.13.2.' ) - // ignore: deprecated_member_use_from_same_package this.hasFloatingPlaceholder = true, this.floatingLabelBehavior = FloatingLabelBehavior.auto, this.hintStyle, @@ -2577,9 +2573,8 @@ class InputDecoration { this.border = InputBorder.none, this.enabled = true, }) : assert(enabled != null), - // ignore: deprecated_member_use_from_same_package assert(!(!hasFloatingPlaceholder && identical(floatingLabelBehavior, FloatingLabelBehavior.always)), - 'hasFloatingPlaceholder=false conflicts with FloatingLabelBehavior.always'), + 'hasFloatingPlaceholder=false conflicts with FloatingLabelBehavior.always'), icon = null, labelText = null, labelStyle = null, @@ -3372,7 +3367,6 @@ class InputDecoration { errorText: errorText ?? this.errorText, errorStyle: errorStyle ?? this.errorStyle, errorMaxLines: errorMaxLines ?? this.errorMaxLines, - // ignore: deprecated_member_use_from_same_package hasFloatingPlaceholder: hasFloatingPlaceholder ?? this.hasFloatingPlaceholder, floatingLabelBehavior: floatingLabelBehavior ?? this.floatingLabelBehavior, isCollapsed: isCollapsed ?? this.isCollapsed, @@ -3420,7 +3414,6 @@ class InputDecoration { hintStyle: hintStyle ?? theme.hintStyle, errorStyle: errorStyle ?? theme.errorStyle, errorMaxLines: errorMaxLines ?? theme.errorMaxLines, - // ignore: deprecated_member_use_from_same_package hasFloatingPlaceholder: hasFloatingPlaceholder ?? theme.hasFloatingPlaceholder, floatingLabelBehavior: floatingLabelBehavior ?? theme.floatingLabelBehavior, isCollapsed: isCollapsed ?? theme.isCollapsed, @@ -3462,7 +3455,6 @@ class InputDecoration { && other.errorText == errorText && other.errorStyle == errorStyle && other.errorMaxLines == errorMaxLines - // ignore: deprecated_member_use_from_same_package && other.hasFloatingPlaceholder == hasFloatingPlaceholder && other.floatingLabelBehavior == floatingLabelBehavior && other.isDense == isDense @@ -3511,7 +3503,7 @@ class InputDecoration { errorText, errorStyle, errorMaxLines, - hasFloatingPlaceholder,// ignore: deprecated_member_use_from_same_package + hasFloatingPlaceholder, floatingLabelBehavior, isDense, contentPadding, @@ -3560,7 +3552,6 @@ class InputDecoration { if (errorText != null) 'errorText: "$errorText"', if (errorStyle != null) 'errorStyle: "$errorStyle"', if (errorMaxLines != null) 'errorMaxLines: "$errorMaxLines"', - // ignore: deprecated_member_use_from_same_package if (hasFloatingPlaceholder == false) 'hasFloatingPlaceholder: false', if (floatingLabelBehavior != null) 'floatingLabelBehavior: $floatingLabelBehavior', if (isDense ?? false) 'isDense: $isDense', @@ -3624,7 +3615,6 @@ class InputDecorationTheme with Diagnosticable { 'Use floatingLabelBehavior instead. ' 'This feature was deprecated after v1.13.2.' ) - // ignore: deprecated_member_use_from_same_package this.hasFloatingPlaceholder = true, this.floatingLabelBehavior = FloatingLabelBehavior.auto, this.isDense = false, @@ -3648,7 +3638,6 @@ class InputDecorationTheme with Diagnosticable { assert(isCollapsed != null), assert(filled != null), assert(alignLabelWithHint != null), - // ignore: deprecated_member_use_from_same_package assert(!(!hasFloatingPlaceholder && identical(floatingLabelBehavior, FloatingLabelBehavior.always)), 'hasFloatingPlaceholder=false conflicts with FloatingLabelBehavior.always'); @@ -4001,7 +3990,6 @@ class InputDecorationTheme with Diagnosticable { hintStyle: hintStyle ?? this.hintStyle, errorStyle: errorStyle ?? this.errorStyle, errorMaxLines: errorMaxLines ?? this.errorMaxLines, - // ignore: deprecated_member_use_from_same_package hasFloatingPlaceholder: hasFloatingPlaceholder ?? this.hasFloatingPlaceholder, floatingLabelBehavior: floatingLabelBehavior ?? this.floatingLabelBehavior, isDense: isDense ?? this.isDense, @@ -4033,7 +4021,6 @@ class InputDecorationTheme with Diagnosticable { hintStyle, errorStyle, errorMaxLines, - // ignore: deprecated_member_use_from_same_package hasFloatingPlaceholder, floatingLabelBehavior, isDense, @@ -4100,7 +4087,6 @@ class InputDecorationTheme with Diagnosticable { properties.add(DiagnosticsProperty('hintStyle', hintStyle, defaultValue: defaultTheme.hintStyle)); properties.add(DiagnosticsProperty('errorStyle', errorStyle, defaultValue: defaultTheme.errorStyle)); properties.add(IntProperty('errorMaxLines', errorMaxLines, defaultValue: defaultTheme.errorMaxLines)); - // ignore: deprecated_member_use_from_same_package properties.add(DiagnosticsProperty('hasFloatingPlaceholder', hasFloatingPlaceholder, defaultValue: defaultTheme.hasFloatingPlaceholder)); properties.add(DiagnosticsProperty('floatingLabelBehavior', floatingLabelBehavior, defaultValue: defaultTheme.floatingLabelBehavior)); properties.add(DiagnosticsProperty('isDense', isDense, defaultValue: defaultTheme.isDense)); diff --git a/packages/flutter/lib/src/material/pickers/calendar_date_range_picker.dart b/packages/flutter/lib/src/material/pickers/calendar_date_range_picker.dart index 01ef1aa4ea4b7..bb018b5be3dcf 100644 --- a/packages/flutter/lib/src/material/pickers/calendar_date_range_picker.dart +++ b/packages/flutter/lib/src/material/pickers/calendar_date_range_picker.dart @@ -29,9 +29,9 @@ const double _maxCalendarWidthPortrait = 480.0; /// Displays a scrollable calendar grid that allows a user to select a range /// of dates. -/// -/// Note: this is not publicly exported (see pickers.dart), as it is an -/// internal component used by [showDateRangePicker]. +// +// This is not publicly exported (see pickers.dart), as it is an +// internal component used by [showDateRangePicker]. class CalendarDateRangePicker extends StatefulWidget { /// Creates a scrollable calendar grid for picking date ranges. CalendarDateRangePicker({ diff --git a/packages/flutter/lib/src/material/pickers/date_picker_deprecated.dart b/packages/flutter/lib/src/material/pickers/date_picker_deprecated.dart index e7831a0f8c48d..c8e4e151f889b 100644 --- a/packages/flutter/lib/src/material/pickers/date_picker_deprecated.dart +++ b/packages/flutter/lib/src/material/pickers/date_picker_deprecated.dart @@ -21,7 +21,7 @@ import '../theme.dart'; import 'date_picker_common.dart'; -// NOTE: this is the original implementation for the Material Date Picker. +// This is the original implementation for the Material Date Picker. // These classes are deprecated and the whole file can be removed after // this has been on stable for long enough for people to migrate to the new // CalendarDatePicker (if needed, as showDatePicker has already been migrated @@ -406,7 +406,6 @@ class MonthPicker extends StatefulWidget { _MonthPickerState createState() => _MonthPickerState(); } -// ignore: deprecated_member_use_from_same_package class _MonthPickerState extends State with SingleTickerProviderStateMixin { static final Animatable _chevronOpacityTween = Tween(begin: 1.0, end: 0.0) .chain(CurveTween(curve: Curves.easeInOut)); @@ -428,7 +427,6 @@ class _MonthPickerState extends State with SingleTickerProviderStat } @override - // ignore: deprecated_member_use_from_same_package void didUpdateWidget(MonthPicker oldWidget) { super.didUpdateWidget(oldWidget); if (widget.selectedDate != oldWidget.selectedDate) { @@ -479,7 +477,6 @@ class _MonthPickerState extends State with SingleTickerProviderStat Widget _buildItems(BuildContext context, int index) { final DateTime month = _addMonthsToMonthDate(widget.firstDate, index); - // ignore: deprecated_member_use_from_same_package return DayPicker( key: ValueKey(month), selectedDate: widget.selectedDate, @@ -675,7 +672,6 @@ class YearPicker extends StatefulWidget { _YearPickerState createState() => _YearPickerState(); } -// ignore: deprecated_member_use_from_same_package class _YearPickerState extends State { static const double _itemExtent = 50.0; ScrollController scrollController; diff --git a/packages/flutter/lib/src/material/pickers/date_picker_header.dart b/packages/flutter/lib/src/material/pickers/date_picker_header.dart index 8cd2878ab7d1f..2409ac9c07b42 100644 --- a/packages/flutter/lib/src/material/pickers/date_picker_header.dart +++ b/packages/flutter/lib/src/material/pickers/date_picker_header.dart @@ -12,7 +12,7 @@ import '../material.dart'; import '../text_theme.dart'; import '../theme.dart'; -// NOTE: This is an internal implementation file. Even though there are public +// This is an internal implementation file. Even though there are public // classes and functions defined here, they are only meant to be used by the // date picker implementation and are not exported as part of the Material library. // See pickers.dart for exactly what is considered part of the public API. diff --git a/packages/flutter/lib/src/material/pickers/date_utils.dart b/packages/flutter/lib/src/material/pickers/date_utils.dart index eb2447b09c212..ef90c0431d900 100644 --- a/packages/flutter/lib/src/material/pickers/date_utils.dart +++ b/packages/flutter/lib/src/material/pickers/date_utils.dart @@ -4,10 +4,9 @@ // @dart = 2.8 - // Common date utility functions used by the date picker implementation -// NOTE: This is an internal implementation file. Even though there are public +// This is an internal implementation file. Even though there are public // classes and functions defined here, they are only meant to be used by the // date picker implementation and are not exported as part of the Material library. // See pickers.dart for exactly what is considered part of the public API. diff --git a/packages/flutter/lib/src/material/pickers/input_date_picker.dart b/packages/flutter/lib/src/material/pickers/input_date_picker.dart index ce936830a29e4..dda971425157a 100644 --- a/packages/flutter/lib/src/material/pickers/input_date_picker.dart +++ b/packages/flutter/lib/src/material/pickers/input_date_picker.dart @@ -244,16 +244,16 @@ class _InputDatePickerFormFieldState extends State { } /// A `TextInputFormatter` set up to format dates. -/// -/// Note: this is not publicly exported (see pickers.dart), as it is -/// just meant for internal use by `InputDatePickerFormField` and -/// `InputDateRangePicker`. +// +// This is not publicly exported (see pickers.dart), as it is +// just meant for internal use by `InputDatePickerFormField` and +// `InputDateRangePicker`. class DateTextInputFormatter extends TextInputFormatter { /// Creates a date formatter with the given separator. DateTextInputFormatter( this.separator - ) : _filterFormatter = WhitelistingTextInputFormatter(RegExp('[\\d$_commonSeparators\\$separator]+')); + ) : _filterFormatter = FilteringTextInputFormatter.allow(RegExp('[\\d$_commonSeparators\\$separator]+')); /// List of common separators that are used in dates. This is used to make /// sure that if given platform's [TextInputType.datetime] keyboard doesn't @@ -267,7 +267,7 @@ class DateTextInputFormatter extends TextInputFormatter { // Formatter that will filter out all characters except digits and date // separators. - final WhitelistingTextInputFormatter _filterFormatter; + final TextInputFormatter _filterFormatter; @override TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { diff --git a/packages/flutter/lib/src/material/pickers/input_date_range_picker.dart b/packages/flutter/lib/src/material/pickers/input_date_range_picker.dart index abfa7fa79c9ee..60297333abe97 100644 --- a/packages/flutter/lib/src/material/pickers/input_date_range_picker.dart +++ b/packages/flutter/lib/src/material/pickers/input_date_range_picker.dart @@ -18,9 +18,9 @@ import 'input_date_picker.dart' show DateTextInputFormatter; /// Provides a pair of text fields that allow the user to enter the start and /// end dates that represent a range of dates. -/// -/// Note: this is not publicly exported (see pickers.dart), as it is just an -/// internal component used by [showDateRangePicker]. +// +// This is not publicly exported (see pickers.dart), as it is just an +// internal component used by [showDateRangePicker]. class InputDateRangePicker extends StatefulWidget { /// Creates a row with two text fields configured to accept the start and end dates /// of a date range. diff --git a/packages/flutter/lib/src/material/pickers/pickers.dart b/packages/flutter/lib/src/material/pickers/pickers.dart index 7dca95ffec142..60bb88307baf6 100644 --- a/packages/flutter/lib/src/material/pickers/pickers.dart +++ b/packages/flutter/lib/src/material/pickers/pickers.dart @@ -15,3 +15,9 @@ export 'date_picker_deprecated.dart'; export 'date_picker_dialog.dart' show showDatePicker; export 'date_range_picker_dialog.dart' show showDateRangePicker; export 'input_date_picker.dart' show InputDatePickerFormField; + +// TODO(ianh): Not exporting everything is unusual and we should +// probably change to just exporting everything and making sure it's +// acceptable as a public API, or, worst case, merging the parts +// that really must be public into a single file and make them +// actually private. diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index 2d0c7f303b455..2027edd08eb79 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -2092,7 +2092,6 @@ class ScaffoldState extends State with TickerProviderStateMixin { // Backwards compatibility for deprecated resizeToAvoidBottomPadding property bool get _resizeToAvoidBottomInset { - // ignore: deprecated_member_use_from_same_package return widget.resizeToAvoidBottomInset ?? widget.resizeToAvoidBottomPadding ?? true; } diff --git a/packages/flutter/lib/src/material/text_theme.dart b/packages/flutter/lib/src/material/text_theme.dart index 37b104c83547f..f82bb62003a3b 100644 --- a/packages/flutter/lib/src/material/text_theme.dart +++ b/packages/flutter/lib/src/material/text_theme.dart @@ -9,10 +9,6 @@ import 'package:flutter/painting.dart'; import 'typography.dart'; -// Eventually we'll get rid of the deprecated members, but for now, we have to use them -// in order to implement them. -// ignore_for_file: deprecated_member_use_from_same_package - /// Material design text theme. /// /// Definitions for the various typographical styles found in Material Design diff --git a/packages/flutter/lib/src/services/asset_bundle.dart b/packages/flutter/lib/src/services/asset_bundle.dart index 5242445146a85..461f88612489b 100644 --- a/packages/flutter/lib/src/services/asset_bundle.dart +++ b/packages/flutter/lib/src/services/asset_bundle.dart @@ -218,7 +218,7 @@ class PlatformAssetBundle extends CachingAssetBundle { Future load(String key) async { final Uint8List encoded = utf8.encoder.convert(Uri(path: Uri.encodeFull(key)).path); final ByteData asset = - await defaultBinaryMessenger.send('flutter/assets', encoded.buffer.asByteData()); // ignore: deprecated_member_use_from_same_package + await defaultBinaryMessenger.send('flutter/assets', encoded.buffer.asByteData()); if (asset == null) throw FlutterError('Unable to load asset: $key'); return asset; diff --git a/packages/flutter/lib/src/services/platform_channel.dart b/packages/flutter/lib/src/services/platform_channel.dart index cb23d29b5025c..74cfe701b86a6 100644 --- a/packages/flutter/lib/src/services/platform_channel.dart +++ b/packages/flutter/lib/src/services/platform_channel.dart @@ -48,7 +48,7 @@ class BasicMessageChannel { final MessageCodec codec; /// The messenger which sends the bytes for this channel, not null. - BinaryMessenger get binaryMessenger => _binaryMessenger ?? defaultBinaryMessenger; // ignore: deprecated_member_use_from_same_package + BinaryMessenger get binaryMessenger => _binaryMessenger ?? defaultBinaryMessenger; final BinaryMessenger _binaryMessenger; /// Sends the specified [message] to the platform plugins on this channel. @@ -142,7 +142,7 @@ class MethodChannel { /// The messenger used by this channel to send platform messages. /// /// The messenger may not be null. - BinaryMessenger get binaryMessenger => _binaryMessenger ?? defaultBinaryMessenger; // ignore: deprecated_member_use_from_same_package + BinaryMessenger get binaryMessenger => _binaryMessenger ?? defaultBinaryMessenger; final BinaryMessenger _binaryMessenger; @optionalTypeArgs @@ -506,7 +506,7 @@ class EventChannel { final MethodCodec codec; /// The messenger used by this channel to send platform messages, not null. - BinaryMessenger get binaryMessenger => _binaryMessenger ?? defaultBinaryMessenger; // ignore: deprecated_member_use_from_same_package + BinaryMessenger get binaryMessenger => _binaryMessenger ?? defaultBinaryMessenger; final BinaryMessenger _binaryMessenger; /// Sets up a broadcast stream for receiving events on this channel. diff --git a/packages/flutter/lib/src/services/text_formatter.dart b/packages/flutter/lib/src/services/text_formatter.dart index 766f9f68449cf..7bcc6ebb44619 100644 --- a/packages/flutter/lib/src/services/text_formatter.dart +++ b/packages/flutter/lib/src/services/text_formatter.dart @@ -6,7 +6,8 @@ import 'dart:math' as math; -import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:flutter/foundation.dart'; + import 'text_editing.dart'; import 'text_input.dart'; @@ -17,10 +18,9 @@ import 'text_input.dart'; /// IME and not on text under composition (i.e., only when /// [TextEditingValue.composing] is collapsed). /// -/// Concrete implementations [BlacklistingTextInputFormatter], which removes -/// blacklisted characters upon edit commit, and -/// [WhitelistingTextInputFormatter], which only allows entries of whitelisted -/// characters, are provided. +/// See also the [FilteringTextInputFormatter], a subclass that +/// removes characters that the user tries to enter if they do, or do +/// not, match a given pattern (as applicable). /// /// To create custom formatters, extend the [TextInputFormatter] class and /// implement the [formatEditUpdate] method. @@ -28,9 +28,7 @@ import 'text_input.dart'; /// See also: /// /// * [EditableText] on which the formatting apply. -/// * [BlacklistingTextInputFormatter], a provided formatter for blacklisting -/// characters. -/// * [WhitelistingTextInputFormatter], a provided formatter for whitelisting +/// * [FilteringTextInputFormatter], a provided formatter for filtering /// characters. abstract class TextInputFormatter { /// Called when text is being typed or cut/copy/pasted in the [EditableText]. @@ -77,34 +75,140 @@ class _SimpleTextInputFormatter extends TextInputFormatter { } } -/// A [TextInputFormatter] that prevents the insertion of blacklisted -/// characters patterns. +/// A [TextInputFormatter] that prevents the insertion of characters +/// matching (or not matching) a particular pattern. /// -/// Instances of blacklisted characters found in the new [TextEditingValue]s +/// Instances of filtered characters found in the new [TextEditingValue]s /// will be replaced with the [replacementString] which defaults to the empty /// string. /// /// Since this formatter only removes characters from the text, it attempts to /// preserve the existing [TextEditingValue.selection] to values it would now /// fall at with the removed characters. -/// -/// See also: -/// -/// * [WhitelistingTextInputFormatter], which uses a whitelist instead of a -/// blacklist. -class BlacklistingTextInputFormatter extends TextInputFormatter { - /// Creates a formatter that prevents the insertion of blacklisted characters patterns. +class FilteringTextInputFormatter extends TextInputFormatter { + /// Creates a formatter that prevents the insertion of characters + /// based on a filter pattern. /// - /// The [blacklistedPattern] must not be null. - BlacklistingTextInputFormatter( - this.blacklistedPattern, { + /// If [allow] is true, then the filter pattern is an allow list, + /// and characters must match the pattern to be accepted. See also + /// [new FilteringTextInputFormatter.allow]. + /// + /// If [allow] is false, then the filter pattern is a deny list, + /// and characters that match the pattern are rejected. See also + /// [new FilteringTextInputFormatter.deny]. + /// + /// The [filterPattern], [allow], and [replacementString] arguments + /// must not be null. + FilteringTextInputFormatter( + this.filterPattern, { + @required this.allow, + this.replacementString = '', + }) : assert(filterPattern != null), + assert(allow != null), + assert(replacementString != null); + + /// Creates a formatter that only allows characters matching a pattern. + /// + /// The [filterPattern] and [replacementString] arguments + /// must not be null. + FilteringTextInputFormatter.allow( + this.filterPattern, { this.replacementString = '', - }) : assert(blacklistedPattern != null); + }) : assert(filterPattern != null), + assert(replacementString != null), + allow = true; - /// A [Pattern] to match and replace incoming [TextEditingValue]s. - final Pattern blacklistedPattern; + /// Creates a formatter that blocks characters matching a pattern. + /// + /// The [filterPattern] and [replacementString] arguments + /// must not be null. + FilteringTextInputFormatter.deny( + this.filterPattern, { + this.replacementString = '', + }) : assert(filterPattern != null), + assert(replacementString != null), + allow = false; - /// String used to replace found patterns. + /// A [Pattern] to match and replace in incoming [TextEditingValue]s. + /// + /// The behaviour of the pattern depends on the [allow] property. If + /// it is true, then this is an allow list, specifying a pattern that + /// characters must match to be accepted. Otherwise, it is a deny list, + /// specifying a pattern that characters must not match to be accepted. + /// + /// In general, the pattern should only match one character at a + /// time. See the discussion at [replacementString]. + /// + /// {@tool snippet} + /// Typically the pattern is a regular expression, as in: + /// + /// ```dart + /// var onlyDigits = FilteringTextInputFormatter.allow(RegExp(r'[0-9]')); + /// ``` + /// {@end-tool} + /// + /// {@tool snippet} + /// If the pattern is a single character, a pattern consisting of a + /// [String] can be used: + /// + /// ```dart + /// var noTabs = FilteringTextInputFormatter.deny('\t'); + /// ``` + /// {@end-tool} + final Pattern filterPattern; + + /// Whether the pattern is an allow list or not. + /// + /// When true, [filterPattern] denotes an allow list: characters + /// must match the filter to be allowed. + /// + /// When false, [filterPattern] denotes a deny list: characters + /// that match the filter are disallowed. + final bool allow; + + /// String used to replace banned patterns. + /// + /// For deny lists ([allow] is false), each match of the + /// [filterPattern] is replaced with this string. If [filterPattern] + /// can match more than one character at a time, then this can + /// result in multiple characters being replaced by a single + /// instance of this [replacementString]. + /// + /// For allow lists ([allow] is true), sequences between matches of + /// [filterPattern] are replaced as one, regardless of the number of + /// characters. + /// + /// For example, consider a [filterPattern] consisting of just the + /// letter "o", applied to text field whose initial value is the + /// string "Into The Woods", with the [replacementString] set to + /// `*`. + /// + /// If [allow] is true, then the result will be "*o*oo*". Each + /// sequence of characters not matching the pattern is replaced by + /// its own single copy of the replacement string, regardless of how + /// many characters are in that sequence. + /// + /// If [allow] is false, then the result will be "Int* the W**ds". + /// Every matching sequence is replaced, and each "o" matches the + /// pattern separately. + /// + /// If the pattern was the [RegExp] `o+`, the result would be the + /// same in the case where [allow] is true, but in the case where + /// [allow] is false, the result would be "Int* the W*ds" (with the + /// two "o"s replaced by a single occurrence of the replacement + /// string) because both of the "o"s would be matched simultaneously + /// by the pattern. + /// + /// Additionally, each segment of the string before, during, and + /// after the current selection in the [TextEditingValue] is handled + /// separately. This means that, in the case of the "Into the Woods" + /// example above, if the selection ended between the two "o"s in + /// "Woods", even if the pattern was `RegExp('o+')`, the result + /// would be "Int* the W**ds", since the two "o"s would be handled + /// in separate passes. + /// + /// See also [String.splitMapJoin], which is used to implement this + /// behavior in both cases. final String replacementString; @override @@ -115,16 +219,87 @@ class BlacklistingTextInputFormatter extends TextInputFormatter { return _selectionAwareTextManipulation( newValue, (String substring) { - return substring.replaceAll(blacklistedPattern, replacementString); + return substring.splitMapJoin( + filterPattern, + onMatch: !allow ? (Match match) => replacementString : null, + onNonMatch: allow ? (String nonMatch) => nonMatch.isNotEmpty ? replacementString : '' : null, + ); }, ); } - /// A [BlacklistingTextInputFormatter] that forces input to be a single line. + /// A [TextInputFormatter] that forces input to be a single line. + static final TextInputFormatter singleLineFormatter = FilteringTextInputFormatter.deny('\n'); + + /// A [TextInputFormatter] that takes in digits `[0-9]` only. + static final TextInputFormatter digitsOnly = FilteringTextInputFormatter.allow(RegExp(r'[0-9]')); +} + +/// Old name for [FilteringTextInputFormatter.deny]. +@Deprecated( + 'Use FilteringTextInputFormatter.deny instead. ' + 'This feature was deprecated after v1.20.0-1.0.pre.' +) +class BlacklistingTextInputFormatter extends FilteringTextInputFormatter { + /// Old name for [FilteringTextInputFormatter.deny]. + @Deprecated( + 'Use FilteringTextInputFormatter.deny instead. ' + 'This feature was deprecated after v1.20.0-1.0.pre.' + ) + BlacklistingTextInputFormatter( + Pattern blacklistedPattern, { + String replacementString = '', + }) : super.deny(blacklistedPattern, replacementString: replacementString); + + /// Old name for [filterPattern]. + @Deprecated( + 'Use filterPattern instead. ' + 'This feature was deprecated after v1.20.0-1.0.pre.' + ) + Pattern get blacklistedPattern => filterPattern; + + /// Old name for [FilteringTextInputFormatter.singleLineFormatter]. + @Deprecated( + 'Use FilteringTextInputFormatter.singleLineFormatter instead. ' + 'This feature was deprecated after v1.20.0-1.0.pre.' + ) static final BlacklistingTextInputFormatter singleLineFormatter = BlacklistingTextInputFormatter(RegExp(r'\n')); } +/// Old name for [FilteringTextInputFormatter.allow]. +// TODO(ianh): Deprecate these once the samples are migrated. +// at-Deprecated( +// 'Use FilteringTextInputFormatter.allow instead. ' +// 'This feature was deprecated after v1.20.0-1.0.pre.' +// ) +class WhitelistingTextInputFormatter extends FilteringTextInputFormatter { + /// Old name for [FilteringTextInputFormatter.allow]. + @Deprecated( + 'Use FilteringTextInputFormatter.allow instead. ' + 'This feature was deprecated after v1.20.0-1.0.pre.' + ) + WhitelistingTextInputFormatter(Pattern whitelistedPattern) + : assert(whitelistedPattern != null), + super.allow(whitelistedPattern); + + /// Old name for [filterPattern]. + @Deprecated( + 'Use filterPattern instead. ' + 'This feature was deprecated after v1.20.0-1.0.pre.' + ) + Pattern get whitelistedPattern => filterPattern; + + /// Old name for [FilteringTextInputFormatter.digitsOnly]. + // TODO(ianh): Deprecate these once the samples are migrated. + // at-Deprecated( + // 'Use FilteringTextInputFormatter.digitsOnly instead. ' + // 'This feature was deprecated after v1.20.0-1.0.pre.' + // ) + static final WhitelistingTextInputFormatter digitsOnly + = WhitelistingTextInputFormatter(RegExp(r'\d+')); +} + /// A [TextInputFormatter] that prevents the insertion of more characters /// (currently defined as Unicode scalar values) than allowed. /// @@ -216,50 +391,6 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter { } } -/// A [TextInputFormatter] that allows only the insertion of whitelisted -/// characters patterns. -/// -/// Since this formatter only removes characters from the text, it attempts to -/// preserve the existing [TextEditingValue.selection] to values it would now -/// fall at with the removed characters. -/// -/// See also: -/// -/// * [BlacklistingTextInputFormatter], which uses a blacklist instead of a -/// whitelist. -class WhitelistingTextInputFormatter extends TextInputFormatter { - /// Creates a formatter that allows only the insertion of whitelisted characters patterns. - /// - /// The [whitelistedPattern] must not be null. - WhitelistingTextInputFormatter(this.whitelistedPattern) - : assert(whitelistedPattern != null); - - /// A [Pattern] to extract all instances of allowed characters. - /// - /// [RegExp] with multiple groups is not supported. - final Pattern whitelistedPattern; - - @override - TextEditingValue formatEditUpdate( - TextEditingValue oldValue, // unused. - TextEditingValue newValue, - ) { - return _selectionAwareTextManipulation( - newValue, - (String substring) { - return whitelistedPattern - .allMatches(substring) - .map((Match match) => match.group(0)) - .join(); - } , - ); - } - - /// A [WhitelistingTextInputFormatter] that takes in digits `[0-9]` only. - static final WhitelistingTextInputFormatter digitsOnly - = WhitelistingTextInputFormatter(RegExp(r'\d+')); -} - TextEditingValue _selectionAwareTextManipulation( TextEditingValue value, String substringManipulation(String substring), diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 0d091fdda6a04..66577bc78a0a9 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -5709,17 +5709,17 @@ class Listener extends StatelessWidget { 'Use MouseRegion.onEnter instead. See MouseRegion.opaque for behavioral difference. ' 'This feature was deprecated after v1.10.14.' ) - this.onPointerEnter, // ignore: deprecated_member_use_from_same_package + this.onPointerEnter, @Deprecated( 'Use MouseRegion.onExit instead. See MouseRegion.opaque for behavioral difference. ' 'This feature was deprecated after v1.10.14.' ) - this.onPointerExit, // ignore: deprecated_member_use_from_same_package + this.onPointerExit, @Deprecated( 'Use MouseRegion.onHover instead. See MouseRegion.opaque for behavioral difference. ' 'This feature was deprecated after v1.10.14.' ) - this.onPointerHover, // ignore: deprecated_member_use_from_same_package + this.onPointerHover, this.onPointerUp, this.onPointerCancel, this.onPointerSignal, diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 2c632fdbfece3..42e7e7f193516 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -460,7 +460,7 @@ class EditableText extends StatefulWidget { keyboardType = keyboardType ?? _inferKeyboardType(autofillHints: autofillHints, maxLines: maxLines), inputFormatters = maxLines == 1 ? [ - BlacklistingTextInputFormatter.singleLineFormatter, + FilteringTextInputFormatter.singleLineFormatter, ...inputFormatters ?? const Iterable.empty(), ] : inputFormatters, diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index 14f3bb8fd1b49..7070ee4c72c56 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -2846,7 +2846,6 @@ void main() { isEmpty: true, decoration: const InputDecoration( border: OutlineInputBorder(borderSide: BorderSide.none), - // ignore: deprecated_member_use_from_same_package hasFloatingPlaceholder: false, labelText: 'label', ), @@ -2871,7 +2870,6 @@ void main() { // isFocused: false (default) decoration: const InputDecoration( border: OutlineInputBorder(borderSide: BorderSide.none), - // ignore: deprecated_member_use_from_same_package hasFloatingPlaceholder: false, labelText: 'label', ), @@ -3941,7 +3939,6 @@ void main() { helperMaxLines: 6, hintStyle: TextStyle(), errorMaxLines: 5, - // ignore: deprecated_member_use_from_same_package hasFloatingPlaceholder: false, floatingLabelBehavior: FloatingLabelBehavior.never, contentPadding: EdgeInsetsDirectional.only(start: 40.0, top: 12.0, bottom: 12.0), diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 78c11fa377c8a..affc8e1c67ccb 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -3157,6 +3157,27 @@ void main() { testWidgets('Injected formatters are chained', (WidgetTester tester) async { final TextEditingController textController = TextEditingController(); + await tester.pumpWidget(boilerplate( + child: TextField( + controller: textController, + decoration: null, + inputFormatters: [ + FilteringTextInputFormatter.deny( + RegExp(r'[a-z]'), + replacementString: '#', + ), + ], + ), + )); + + await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六'); + // The default single line formatter replaces \n with empty string. + expect(textController.text, '#一#二#三#四#五#六'); + }); + + testWidgets('Injected formatters are chained (deprecated names)', (WidgetTester tester) async { + final TextEditingController textController = TextEditingController(); + await tester.pumpWidget(boilerplate( child: TextField( controller: textController, @@ -3178,6 +3199,33 @@ void main() { testWidgets('Chained formatters are in sequence', (WidgetTester tester) async { final TextEditingController textController = TextEditingController(); + await tester.pumpWidget(boilerplate( + child: TextField( + controller: textController, + decoration: null, + maxLines: 2, + inputFormatters: [ + FilteringTextInputFormatter.deny( + RegExp(r'[a-z]'), + replacementString: '12\n', + ), + FilteringTextInputFormatter.allow(RegExp(r'\n[0-9]')), + ], + ), + )); + + await tester.enterText(find.byType(TextField), 'a1b2c3'); + // The first formatter turns it into + // 12\n112\n212\n3 + // The second formatter turns it into + // \n1\n2\n3 + // Multiline is allowed since maxLine != 1. + expect(textController.text, '\n1\n2\n3'); + }); + + testWidgets('Chained formatters are in sequence (deprecated names)', (WidgetTester tester) async { + final TextEditingController textController = TextEditingController(); + await tester.pumpWidget(boilerplate( child: TextField( controller: textController, @@ -3205,6 +3253,44 @@ void main() { testWidgets('Pasted values are formatted', (WidgetTester tester) async { final TextEditingController textController = TextEditingController(); + await tester.pumpWidget( + overlay( + child: TextField( + controller: textController, + decoration: null, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + ), + ), + ); + + await tester.enterText(find.byType(TextField), 'a1b\n2c3'); + expect(textController.text, '123'); + await skipPastScrollingAnimation(tester); + + await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2'))); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero + final RenderEditable renderEditable = findRenderEditable(tester); + final List endpoints = globalize( + renderEditable.getEndpointsForSelection(textController.selection), + renderEditable, + ); + await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero + + Clipboard.setData(const ClipboardData(text: '一4二\n5三6')); + await tester.tap(find.text('PASTE')); + await tester.pump(); + // Puts 456 before the 2 in 123. + expect(textController.text, '145623'); + }); + + testWidgets('Pasted values are formatted (deprecated names)', (WidgetTester tester) async { + final TextEditingController textController = TextEditingController(); + await tester.pumpWidget( overlay( child: TextField( @@ -3473,7 +3559,28 @@ void main() { expect(textController.text, '0123456789'); }); - testWidgets('maxLength still works with other formatters.', (WidgetTester tester) async { + testWidgets('maxLength still works with other formatters', (WidgetTester tester) async { + final TextEditingController textController = TextEditingController(); + + await tester.pumpWidget(boilerplate( + child: TextField( + controller: textController, + maxLength: 10, + inputFormatters: [ + FilteringTextInputFormatter.deny( + RegExp(r'[a-z]'), + replacementString: '#', + ), + ], + ), + )); + + await tester.enterText(find.byType(TextField), 'a一b二c三\nd四e五f六'); + // The default single line formatter replaces \n with empty string. + expect(textController.text, '#一#二#三#四#五'); + }); + + testWidgets('maxLength still works with other formatters (deprecated names)', (WidgetTester tester) async { final TextEditingController textController = TextEditingController(); await tester.pumpWidget(boilerplate( diff --git a/packages/flutter/test/services/text_formatter_test.dart b/packages/flutter/test/services/text_formatter_test.dart index e90966fb7cdf5..dcbb9e7614f00 100644 --- a/packages/flutter/test/services/text_formatter_test.dart +++ b/packages/flutter/test/services/text_formatter_test.dart @@ -86,4 +86,148 @@ void main() { }); }); }); + + test('FilteringTextInputFormatter should return the old value if new value contains non-white-listed character', () { + const TextEditingValue oldValue = TextEditingValue(text: '12345'); + const TextEditingValue newValue = TextEditingValue(text: '12345@'); + + final TextInputFormatter formatter = FilteringTextInputFormatter.digitsOnly; + final TextEditingValue formatted = formatter.formatEditUpdate(oldValue, newValue); + + // assert that we are passing digits only at the first time + expect(oldValue.text, equals('12345')); + // The new value is always the oldValue plus a non-digit character (user press @) + expect(newValue.text, equals('12345@')); + // we expect that the formatted value returns the oldValue only since the newValue does not + // satisfy the formatter condition (which is, in this case, digitsOnly) + expect(formatted.text, equals('12345')); + }); + + test('FilteringTextInputFormatter should move the cursor to the right position', () { + TextEditingValue collapsedValue(String text, int offset) => + TextEditingValue( + text: text, + selection: TextSelection.collapsed(offset: offset), + ); + + TextEditingValue oldValue = collapsedValue('123', 0); + TextEditingValue newValue = collapsedValue('123456', 6); + + final TextInputFormatter formatter = FilteringTextInputFormatter.digitsOnly; + TextEditingValue formatted = formatter.formatEditUpdate(oldValue, newValue); + + // assert that we are passing digits only at the first time + expect(oldValue.text, equals('123')); + // assert that we are passing digits only at the second time + expect(newValue.text, equals('123456')); + // assert that cursor is at the end of the text + expect(formatted.selection.baseOffset, equals(6)); + + // move cursor at the middle of the text and then add the number 9. + oldValue = newValue.copyWith(selection: const TextSelection.collapsed(offset: 4)); + newValue = oldValue.copyWith(text: '1239456'); + + formatted = formatter.formatEditUpdate(oldValue, newValue); + + // cursor must be now at fourth position (right after the number 9) + expect(formatted.selection.baseOffset, equals(4)); + }); + + test('FilteringTextInputFormatter should remove non-allowed characters', () { + const TextEditingValue oldValue = TextEditingValue(text: '12345'); + const TextEditingValue newValue = TextEditingValue(text: '12345@'); + + final TextInputFormatter formatter = FilteringTextInputFormatter.digitsOnly; + final TextEditingValue formatted = formatter.formatEditUpdate(oldValue, newValue); + + // assert that we are passing digits only at the first time + expect(oldValue.text, equals('12345')); + // The new value is always the oldValue plus a non-digit character (user press @) + expect(newValue.text, equals('12345@')); + // we expect that the formatted value returns the oldValue only since the difference + // between the oldValue and the newValue is only material that isn't allowed + expect(formatted.text, equals('12345')); + }); + + test('WhitelistingTextInputFormatter should return the old value if new value contains non-allowed character', () { + const TextEditingValue oldValue = TextEditingValue(text: '12345'); + const TextEditingValue newValue = TextEditingValue(text: '12345@'); + + final WhitelistingTextInputFormatter formatter = WhitelistingTextInputFormatter.digitsOnly; + final TextEditingValue formatted = formatter.formatEditUpdate(oldValue, newValue); + + // assert that we are passing digits only at the first time + expect(oldValue.text, equals('12345')); + // The new value is always the oldValue plus a non-digit character (user press @) + expect(newValue.text, equals('12345@')); + // we expect that the formatted value returns the oldValue only since the newValue does not + // satisfy the formatter condition (which is, in this case, digitsOnly) + expect(formatted.text, equals('12345')); + }); + + test('FilteringTextInputFormatter should move the cursor to the right position', () { + TextEditingValue collapsedValue(String text, int offset) => + TextEditingValue( + text: text, + selection: TextSelection.collapsed(offset: offset), + ); + + TextEditingValue oldValue = collapsedValue('123', 0); + TextEditingValue newValue = collapsedValue('123456', 6); + + final TextInputFormatter formatter = + FilteringTextInputFormatter.digitsOnly; + TextEditingValue formatted = formatter.formatEditUpdate(oldValue, + newValue); + + // assert that we are passing digits only at the first time + expect(oldValue.text, equals('123')); + // assert that we are passing digits only at the second time + expect(newValue.text, equals('123456')); + // assert that cursor is at the end of the text + expect(formatted.selection.baseOffset, equals(6)); + + // move cursor at the middle of the text and then add the number 9. + oldValue = newValue.copyWith( + selection: const TextSelection.collapsed(offset: 4)); + newValue = oldValue.copyWith(text: '1239456'); + + formatted = formatter.formatEditUpdate(oldValue, newValue); + + // cursor must be now at fourth position (right after the number 9) + expect(formatted.selection.baseOffset, equals(4)); + }); + + test('WhitelistingTextInputFormatter should move the cursor to the right position', () { + TextEditingValue collapsedValue(String text, int offset) => + TextEditingValue( + text: text, + selection: TextSelection.collapsed(offset: offset), + ); + + TextEditingValue oldValue = collapsedValue('123', 0); + TextEditingValue newValue = collapsedValue('123456', 6); + + final WhitelistingTextInputFormatter formatter = + WhitelistingTextInputFormatter.digitsOnly; + TextEditingValue formatted = formatter.formatEditUpdate(oldValue, + newValue); + + // assert that we are passing digits only at the first time + expect(oldValue.text, equals('123')); + // assert that we are passing digits only at the second time + expect(newValue.text, equals('123456')); + // assert that cursor is at the end of the text + expect(formatted.selection.baseOffset, equals(6)); + + // move cursor at the middle of the text and then add the number 9. + oldValue = newValue.copyWith( + selection: const TextSelection.collapsed(offset: 4)); + newValue = oldValue.copyWith(text: '1239456'); + + formatted = formatter.formatEditUpdate(oldValue, newValue); + + // cursor must be now at fourth position (right after the number 9) + expect(formatted.selection.baseOffset, equals(4)); + }); } diff --git a/packages/flutter/test/widgets/listener_deprecated_test.dart b/packages/flutter/test/widgets/listener_deprecated_test.dart index cf2b7e360fcce..d8e8a51dda0ea 100644 --- a/packages/flutter/test/widgets/listener_deprecated_test.dart +++ b/packages/flutter/test/widgets/listener_deprecated_test.dart @@ -13,10 +13,6 @@ import 'package:flutter/gestures.dart'; // The tests in this file are moved from listener_test.dart, which tests several // deprecated APIs. The file should be removed once these parameters are. -// ignore_for_file: deprecated_member_use_from_same_package -// We have to ignore the lint rule here because we need to use the deprecated -// callbacks in order to test them. - class HoverClient extends StatefulWidget { const HoverClient({Key key, this.onHover, this.child}) : super(key: key); diff --git a/packages/flutter/test/widgets/text_formatter_test.dart b/packages/flutter/test/widgets/text_formatter_test.dart index b37d8a3fdf741..0da0dd13bf5a3 100644 --- a/packages/flutter/test/widgets/text_formatter_test.dart +++ b/packages/flutter/test/widgets/text_formatter_test.dart @@ -9,11 +9,10 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; void main() { - TextEditingValue testOldValue; + const TextEditingValue testOldValue = TextEditingValue(); TextEditingValue testNewValue; test('withFunction wraps formatting function', () { - testOldValue = const TextEditingValue(); testNewValue = const TextEditingValue(); TextEditingValue calledOldValue; @@ -47,7 +46,62 @@ void main() { ); }); - test('test blacklisting formatter', () { + test('test filtering formatter example', () { + const TextEditingValue intoTheWoods = TextEditingValue(text: 'Into the Woods'); + expect( + FilteringTextInputFormatter('o', allow: true, replacementString: '*').formatEditUpdate(testOldValue, intoTheWoods), + const TextEditingValue(text: '*o*oo*'), + ); + expect( + FilteringTextInputFormatter('o', allow: false, replacementString: '*').formatEditUpdate(testOldValue, intoTheWoods), + const TextEditingValue(text: 'Int* the W**ds'), + ); + expect( + FilteringTextInputFormatter(RegExp('o+'), allow: true, replacementString: '*').formatEditUpdate(testOldValue, intoTheWoods), + const TextEditingValue(text: '*o*oo*'), + ); + expect( + FilteringTextInputFormatter(RegExp('o+'), allow: false, replacementString: '*').formatEditUpdate(testOldValue, intoTheWoods), + const TextEditingValue(text: 'Int* the W*ds'), + ); + + const TextEditingValue selectedIntoTheWoods = TextEditingValue(text: 'Into the Woods', selection: TextSelection(baseOffset: 11, extentOffset: 14)); + expect( + FilteringTextInputFormatter('o', allow: true, replacementString: '*').formatEditUpdate(testOldValue, selectedIntoTheWoods), + const TextEditingValue(text: '*o*oo*', selection: TextSelection(baseOffset: 4, extentOffset: 6)), + ); + expect( + FilteringTextInputFormatter('o', allow: false, replacementString: '*').formatEditUpdate(testOldValue, selectedIntoTheWoods), + const TextEditingValue(text: 'Int* the W**ds', selection: TextSelection(baseOffset: 11, extentOffset: 14)), + ); + expect( + FilteringTextInputFormatter(RegExp('o+'), allow: true, replacementString: '*').formatEditUpdate(testOldValue, selectedIntoTheWoods), + const TextEditingValue(text: '*o*oo*', selection: TextSelection(baseOffset: 4, extentOffset: 6)), + ); + expect( + FilteringTextInputFormatter(RegExp('o+'), allow: false, replacementString: '*').formatEditUpdate(testOldValue, selectedIntoTheWoods), + const TextEditingValue(text: 'Int* the W**ds', selection: TextSelection(baseOffset: 11, extentOffset: 14)), + ); + }); + + test('test filtering formatter, deny mode', () { + final TextEditingValue actualValue = + FilteringTextInputFormatter.deny(RegExp(r'[a-z]')) + .formatEditUpdate(testOldValue, testNewValue); + + // Expecting + // 1(23 + // 4)56 + expect(actualValue, const TextEditingValue( + text: '123\n456', + selection: TextSelection( + baseOffset: 1, + extentOffset: 5, + ), + )); + }); + + test('test filtering formatter, deny mode (deprecated names)', () { final TextEditingValue actualValue = BlacklistingTextInputFormatter(RegExp(r'[a-z]')) .formatEditUpdate(testOldValue, testNewValue); @@ -65,6 +119,22 @@ void main() { }); test('test single line formatter', () { + final TextEditingValue actualValue = + FilteringTextInputFormatter.singleLineFormatter + .formatEditUpdate(testOldValue, testNewValue); + + // Expecting + // a1b(2c3d4)e5f6 + expect(actualValue, const TextEditingValue( + text: 'a1b2c3d4e5f6', + selection: TextSelection( + baseOffset: 3, + extentOffset: 8, + ), + )); + }); + + test('test single line formatter (deprecated names)', () { final TextEditingValue actualValue = BlacklistingTextInputFormatter.singleLineFormatter .formatEditUpdate(testOldValue, testNewValue); @@ -80,7 +150,23 @@ void main() { )); }); - test('test whitelisting formatter', () { + test('test filtering formatter, allow mode', () { + final TextEditingValue actualValue = + FilteringTextInputFormatter.allow(RegExp(r'[a-c]')) + .formatEditUpdate(testOldValue, testNewValue); + + // Expecting + // ab(c) + expect(actualValue, const TextEditingValue( + text: 'abc', + selection: TextSelection( + baseOffset: 2, + extentOffset: 3, + ), + )); + }); + + test('test filtering formatter, allow mode (deprecated names)', () { final TextEditingValue actualValue = WhitelistingTextInputFormatter(RegExp(r'[a-c]')) .formatEditUpdate(testOldValue, testNewValue); @@ -97,6 +183,22 @@ void main() { }); test('test digits only formatter', () { + final TextEditingValue actualValue = + FilteringTextInputFormatter.digitsOnly + .formatEditUpdate(testOldValue, testNewValue); + + // Expecting + // 1(234)56 + expect(actualValue, const TextEditingValue( + text: '123456', + selection: TextSelection( + baseOffset: 1, + extentOffset: 4, + ), + )); + }); + + test('test digits only formatter (deprecated names)', () { final TextEditingValue actualValue = WhitelistingTextInputFormatter.digitsOnly .formatEditUpdate(testOldValue, testNewValue); @@ -246,6 +348,5 @@ void main() { ), )); }); - }); } diff --git a/packages/flutter_driver/lib/src/extension/extension.dart b/packages/flutter_driver/lib/src/extension/extension.dart index c7bbdcf9efd5d..135c888694a67 100644 --- a/packages/flutter_driver/lib/src/extension/extension.dart +++ b/packages/flutter_driver/lib/src/extension/extension.dart @@ -129,9 +129,9 @@ class FlutterDriverExtension { 'waitFor': _waitFor, 'waitForAbsent': _waitForAbsent, 'waitForCondition': _waitForCondition, - 'waitUntilNoTransientCallbacks': _waitUntilNoTransientCallbacks, // ignore: deprecated_member_use_from_same_package - 'waitUntilNoPendingFrame': _waitUntilNoPendingFrame, // ignore: deprecated_member_use_from_same_package - 'waitUntilFirstFrameRasterized': _waitUntilFirstFrameRasterized, // ignore: deprecated_member_use_from_same_package + 'waitUntilNoTransientCallbacks': _waitUntilNoTransientCallbacks, + 'waitUntilNoPendingFrame': _waitUntilNoPendingFrame, + 'waitUntilFirstFrameRasterized': _waitUntilFirstFrameRasterized, 'get_semantics_id': _getSemanticsId, 'get_offset': _getOffset, 'get_diagnostics_tree': _getDiagnosticsTree, @@ -153,9 +153,9 @@ class FlutterDriverExtension { 'waitFor': (Map params) => WaitFor.deserialize(params), 'waitForAbsent': (Map params) => WaitForAbsent.deserialize(params), 'waitForCondition': (Map params) => WaitForCondition.deserialize(params), - 'waitUntilNoTransientCallbacks': (Map params) => WaitUntilNoTransientCallbacks.deserialize(params), // ignore: deprecated_member_use_from_same_package - 'waitUntilNoPendingFrame': (Map params) => WaitUntilNoPendingFrame.deserialize(params), // ignore: deprecated_member_use_from_same_package - 'waitUntilFirstFrameRasterized': (Map params) => WaitUntilFirstFrameRasterized.deserialize(params), // ignore: deprecated_member_use_from_same_package + 'waitUntilNoTransientCallbacks': (Map params) => WaitUntilNoTransientCallbacks.deserialize(params), + 'waitUntilNoPendingFrame': (Map params) => WaitUntilNoPendingFrame.deserialize(params), + 'waitUntilFirstFrameRasterized': (Map params) => WaitUntilFirstFrameRasterized.deserialize(params), 'get_semantics_id': (Map params) => GetSemanticsId.deserialize(params), 'get_offset': (Map params) => GetOffset.deserialize(params), 'get_diagnostics_tree': (Map params) => GetDiagnosticsTree.deserialize(params), diff --git a/packages/flutter_driver/test/src/real_tests/extension_test.dart b/packages/flutter_driver/test/src/real_tests/extension_test.dart index 30e52eaa32968..b00abea68bdc7 100644 --- a/packages/flutter_driver/test/src/real_tests/extension_test.dart +++ b/packages/flutter_driver/test/src/real_tests/extension_test.dart @@ -41,7 +41,7 @@ void main() { }); testWidgets('returns immediately when transient callback queue is empty', (WidgetTester tester) async { - extension.call(const WaitUntilNoTransientCallbacks().serialize()) // ignore: deprecated_member_use_from_same_package + extension.call(const WaitUntilNoTransientCallbacks().serialize()) .then(expectAsync1((Map r) { result = r; })); @@ -61,7 +61,7 @@ void main() { // Intentionally blank. We only care about existence of a callback. }); - extension.call(const WaitUntilNoTransientCallbacks().serialize()) // ignore: deprecated_member_use_from_same_package + extension.call(const WaitUntilNoTransientCallbacks().serialize()) .then(expectAsync1((Map r) { result = r; })); @@ -888,7 +888,7 @@ void main() { testWidgets('returns immediately when frame is synced', ( WidgetTester tester) async { - extension.call(const WaitUntilNoPendingFrame().serialize()) // ignore: deprecated_member_use_from_same_package + extension.call(const WaitUntilNoPendingFrame().serialize()) .then(expectAsync1((Map r) { result = r; })); @@ -909,7 +909,7 @@ void main() { // Intentionally blank. We only care about existence of a callback. }); - extension.call(const WaitUntilNoPendingFrame().serialize()) // ignore: deprecated_member_use_from_same_package + extension.call(const WaitUntilNoPendingFrame().serialize()) .then(expectAsync1((Map r) { result = r; })); @@ -933,7 +933,7 @@ void main() { 'waits until no pending scheduled frame', (WidgetTester tester) async { SchedulerBinding.instance.scheduleFrame(); - extension.call(const WaitUntilNoPendingFrame().serialize()) // ignore: deprecated_member_use_from_same_package + extension.call(const WaitUntilNoPendingFrame().serialize()) .then(expectAsync1((Map r) { result = r; }));