diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart index 4a8ac61587..01b0019019 100644 --- a/packages/flutter/lib/src/cupertino/text_field.dart +++ b/packages/flutter/lib/src/cupertino/text_field.dart @@ -260,6 +260,7 @@ class CupertinoTextField extends StatefulWidget { this.expands = false, this.maxLength, this.maxLengthEnforced = true, + this.maxLengthEnforcement, this.onChanged, this.onEditingComplete, this.onSubmitted, @@ -292,6 +293,10 @@ class CupertinoTextField extends StatefulWidget { smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), assert(enableSuggestions != null), assert(maxLengthEnforced != null), + assert( + maxLengthEnforced || maxLengthEnforcement == null, + 'maxLengthEnforced is deprecated, use only maxLengthEnforcement', + ), assert(scrollPadding != null), assert(dragStartBehavior != null), assert(selectionHeightStyle != null), @@ -402,6 +407,7 @@ class CupertinoTextField extends StatefulWidget { this.expands = false, this.maxLength, this.maxLengthEnforced = true, + this.maxLengthEnforcement, this.onChanged, this.onEditingComplete, this.onSubmitted, @@ -434,6 +440,10 @@ class CupertinoTextField extends StatefulWidget { smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), assert(enableSuggestions != null), assert(maxLengthEnforced != null), + assert( + maxLengthEnforced || maxLengthEnforcement == null, + 'maxLengthEnforced is deprecated, use only maxLengthEnforcement', + ), assert(scrollPadding != null), assert(dragStartBehavior != null), assert(selectionHeightStyle != null), @@ -619,12 +629,13 @@ class CupertinoTextField extends StatefulWidget { /// The maximum number of characters (Unicode scalar values) to allow in the /// text field. /// - /// If set, a character counter will be displayed below the - /// field, showing how many characters have been entered and how many are - /// allowed. After [maxLength] characters have been input, additional input - /// is ignored, unless [maxLengthEnforced] is set to false. The TextField - /// enforces the length with a [LengthLimitingTextInputFormatter], which is - /// evaluated after the supplied [inputFormatters], if any. + /// After [maxLength] characters have been input, additional input + /// is ignored, unless [maxLengthEnforcement] is set to + /// [MaxLengthEnforcement.none]. + /// + /// The TextField enforces the length with a + /// [LengthLimitingTextInputFormatter], which is evaluated after the supplied + /// [inputFormatters], if any. /// /// This value must be either null or greater than zero. If set to null /// (the default), there is no limit to the number of characters allowed. @@ -635,14 +646,23 @@ class CupertinoTextField extends StatefulWidget { /// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength} final int? maxLength; + /// If [maxLength] is set, [maxLengthEnforced] indicates whether or not to + /// enforce the limit. + /// /// If true, prevents the field from allowing more than [maxLength] /// characters. - /// - /// If [maxLength] is set, [maxLengthEnforced] indicates whether or not to - /// enforce the limit, or merely provide a character counter and warning when - /// [maxLength] is exceeded. final bool maxLengthEnforced; + /// Determines how the [maxLength] limit should be enforced. + /// + /// If [MaxLengthEnforcement.none] is set, additional input beyond [maxLength] + /// will not be enforced by the limit. + /// + /// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement} + /// + /// {@macro flutter.services.textFormatter.maxLengthEnforcement} + final MaxLengthEnforcement? maxLengthEnforcement; + /// {@macro flutter.widgets.editableText.onChanged} final ValueChanged? onChanged; @@ -761,6 +781,7 @@ class CupertinoTextField extends StatefulWidget { properties.add(DiagnosticsProperty('expands', expands, defaultValue: false)); properties.add(IntProperty('maxLength', maxLength, defaultValue: null)); properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, ifTrue: 'max length enforced')); + properties.add(EnumProperty('maxLengthEnforcement', maxLengthEnforcement, defaultValue: null)); properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)); properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null)); properties.add(DiagnosticsProperty('cursorRadius', cursorRadius, defaultValue: null)); @@ -783,6 +804,9 @@ class _CupertinoTextFieldState extends State with Restoratio FocusNode? _focusNode; FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); + MaxLengthEnforcement get _effectiveMaxLengthEnforcement => widget.maxLengthEnforcement + ?? LengthLimitingTextInputFormatter.inferredDefaultMaxLengthEnforcement; + bool _showSelectionHandles = false; late _CupertinoTextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder; @@ -1030,7 +1054,11 @@ class _CupertinoTextFieldState extends State with Restoratio final Offset cursorOffset = Offset(_iOSHorizontalCursorOffsetPixels / MediaQuery.of(context).devicePixelRatio, 0); final List formatters = [ ...?widget.inputFormatters, - if (widget.maxLength != null && widget.maxLengthEnforced) LengthLimitingTextInputFormatter(widget.maxLength) + if (widget.maxLength != null && widget.maxLengthEnforced) + LengthLimitingTextInputFormatter( + widget.maxLength, + maxLengthEnforcement: _effectiveMaxLengthEnforcement, + ), ]; final CupertinoThemeData themeData = CupertinoTheme.of(context); diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 98c1f78bfb..cc17536624 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -302,10 +302,11 @@ class TextField extends StatefulWidget { /// [TextField.noMaxLength] then only the current length is displayed. /// /// After [maxLength] characters have been input, additional input - /// is ignored, unless [maxLengthEnforced] is set to false. The text field - /// enforces the length with a [LengthLimitingTextInputFormatter], which is - /// evaluated after the supplied [inputFormatters], if any. The [maxLength] - /// value must be either null or greater than zero. + /// is ignored, unless [maxLengthEnforcement] is set to + /// [MaxLengthEnforcement.none]. + /// The text field enforces the length with a [LengthLimitingTextInputFormatter], + /// which is evaluated after the supplied [inputFormatters], if any. + /// The [maxLength] value must be either null or greater than zero. /// /// If [maxLengthEnforced] is set to false, then more than [maxLength] /// characters may be entered, and the error counter and divider will @@ -356,6 +357,7 @@ class TextField extends StatefulWidget { this.expands = false, this.maxLength, this.maxLengthEnforced = true, + this.maxLengthEnforcement, this.onChanged, this.onEditingComplete, this.onSubmitted, @@ -391,6 +393,10 @@ class TextField extends StatefulWidget { assert(enableSuggestions != null), assert(enableInteractiveSelection != null), assert(maxLengthEnforced != null), + assert( + maxLengthEnforced || maxLengthEnforcement == null, + 'maxLengthEnforced is deprecated, use only maxLengthEnforcement', + ), assert(scrollPadding != null), assert(dragStartBehavior != null), assert(selectionHeightStyle != null), @@ -568,9 +574,11 @@ class TextField extends StatefulWidget { /// to [TextField.noMaxLength] then only the current character count is displayed. /// /// After [maxLength] characters have been input, additional input - /// is ignored, unless [maxLengthEnforced] is set to false. The text field - /// enforces the length with a [LengthLimitingTextInputFormatter], which is - /// evaluated after the supplied [inputFormatters], if any. + /// is ignored, unless [maxLengthEnforcement] is set to + /// [MaxLengthEnforcement.none]. + /// + /// The text field enforces the length with a [LengthLimitingTextInputFormatter], + /// which is evaluated after the supplied [inputFormatters], if any. /// /// This value must be either null, [TextField.noMaxLength], or greater than 0. /// If null (the default) then there is no limit to the number of characters @@ -580,7 +588,8 @@ class TextField extends StatefulWidget { /// Whitespace characters (e.g. newline, space, tab) are included in the /// character count. /// - /// If [maxLengthEnforced] is set to false, then more than [maxLength] + /// If [maxLengthEnforced] is set to false or [maxLengthEnforcement] is + /// [MaxLengthEnforcement.none], then more than [maxLength] /// characters may be entered, but the error counter and divider will switch /// to the [decoration]'s [InputDecoration.errorStyle] when the limit is /// exceeded. @@ -588,14 +597,21 @@ class TextField extends StatefulWidget { /// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength} final int? maxLength; - /// If true, prevents the field from allowing more than [maxLength] - /// characters. - /// /// If [maxLength] is set, [maxLengthEnforced] indicates whether or not to /// enforce the limit, or merely provide a character counter and warning when /// [maxLength] is exceeded. + /// + /// If true, prevents the field from allowing more than [maxLength] + /// characters. final bool maxLengthEnforced; + /// Determines how the [maxLength] limit should be enforced. + /// + /// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement} + /// + /// {@macro flutter.services.textFormatter.maxLengthEnforcement} + final MaxLengthEnforcement? maxLengthEnforcement; + /// {@macro flutter.widgets.editableText.onChanged} /// /// See also: @@ -810,6 +826,7 @@ class TextField extends StatefulWidget { properties.add(DiagnosticsProperty('expands', expands, defaultValue: false)); properties.add(IntProperty('maxLength', maxLength, defaultValue: null)); properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, defaultValue: true, ifFalse: 'maxLength not enforced')); + properties.add(EnumProperty('maxLengthEnforcement', maxLengthEnforcement, defaultValue: null)); properties.add(EnumProperty('textInputAction', textInputAction, defaultValue: null)); properties.add(EnumProperty('textCapitalization', textCapitalization, defaultValue: TextCapitalization.none)); properties.add(EnumProperty('textAlign', textAlign, defaultValue: TextAlign.start)); @@ -835,6 +852,9 @@ class _TextFieldState extends State with RestorationMixin implements FocusNode? _focusNode; FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); + MaxLengthEnforcement get _effectiveMaxLengthEnforcement => widget.maxLengthEnforcement + ?? LengthLimitingTextInputFormatter.inferredDefaultMaxLengthEnforcement; + bool _isHovering = false; bool get needsCounter => widget.maxLength != null @@ -1095,7 +1115,11 @@ class _TextFieldState extends State with RestorationMixin implements final FocusNode focusNode = _effectiveFocusNode; final List formatters = [ ...?widget.inputFormatters, - if (widget.maxLength != null && widget.maxLengthEnforced) LengthLimitingTextInputFormatter(widget.maxLength) + if (widget.maxLength != null && widget.maxLengthEnforced) + LengthLimitingTextInputFormatter( + widget.maxLength, + maxLengthEnforcement: _effectiveMaxLengthEnforcement, + ), ]; TextSelectionControls? textSelectionControls = widget.selectionControls; @@ -1226,6 +1250,16 @@ class _TextFieldState extends State with RestorationMixin implements }, ); + final int? semanticsMaxValueLength; + if (widget.maxLengthEnforced && + _effectiveMaxLengthEnforcement != MaxLengthEnforcement.none && + widget.maxLength != null && + widget.maxLength! > 0) { + semanticsMaxValueLength = widget.maxLength; + } else { + semanticsMaxValueLength = null; + } + return MouseRegion( cursor: effectiveMouseCursor, onEnter: (PointerEnterEvent event) => _handleHover(true), @@ -1236,9 +1270,7 @@ class _TextFieldState extends State with RestorationMixin implements animation: controller, // changes the _currentLength builder: (BuildContext context, Widget? child) { return Semantics( - maxValueLength: widget.maxLengthEnforced && widget.maxLength != null && widget.maxLength! > 0 - ? widget.maxLength - : null, + maxValueLength: semanticsMaxValueLength, currentValueLength: _currentLength, onTap: () { if (!_effectiveController.selection.isValid) diff --git a/packages/flutter/lib/src/material/text_form_field.dart b/packages/flutter/lib/src/material/text_form_field.dart index 071f2d793f..d8b16b1e93 100644 --- a/packages/flutter/lib/src/material/text_form_field.dart +++ b/packages/flutter/lib/src/material/text_form_field.dart @@ -163,6 +163,7 @@ class TextFormField extends FormField { ) bool autovalidate = false, bool maxLengthEnforced = true, + MaxLengthEnforcement? maxLengthEnforcement, int? maxLines = 1, int? minLines, bool expands = false, @@ -202,6 +203,10 @@ class TextFormField extends FormField { 'autovalidate and autovalidateMode should not be used together.' ), assert(maxLengthEnforced != null), + assert( + maxLengthEnforced || maxLengthEnforcement == null, + 'maxLengthEnforced is deprecated, use only maxLengthEnforcement', + ), assert(scrollPadding != null), assert(maxLines == null || maxLines > 0), assert(minLines == null || minLines > 0), @@ -259,6 +264,7 @@ class TextFormField extends FormField { smartQuotesType: smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), enableSuggestions: enableSuggestions, maxLengthEnforced: maxLengthEnforced, + maxLengthEnforcement: maxLengthEnforcement, maxLines: maxLines, minLines: minLines, expands: expands, diff --git a/packages/flutter/lib/src/services/text_formatter.dart b/packages/flutter/lib/src/services/text_formatter.dart index 9a5349cb39..d1ad8edfe5 100644 --- a/packages/flutter/lib/src/services/text_formatter.dart +++ b/packages/flutter/lib/src/services/text_formatter.dart @@ -11,6 +11,49 @@ import 'package:flutter/foundation.dart'; import 'text_editing.dart'; import 'text_input.dart'; +/// {@template flutter.services.textFormatter.maxLengthEnforcement} +/// ### [MaxLengthEnforcement.enforced] versus +/// [MaxLengthEnforcement.truncateAfterCompositionEnds] +/// +/// Both [MaxLengthEnforcement.enforced] and +/// [MaxLengthEnforcement.truncateAfterCompositionEnds] make sure the final +/// length of the text does not exceed the max length specified. The difference +/// is that [MaxLengthEnforcement.enforced] truncates all text while +/// [MaxLengthEnforcement.truncateAfterCompositionEnds] allows composing text to +/// exceed the limit. Allowing this "placeholder" composing text to exceed the +/// limit may provide a better user experience on some platforms for entering +/// ideographic characters (e.g. CJK characters) via composing on phonetic +/// keyboards. +/// +/// Some input methods (Gboard on Android for example) initiate text composition +/// even for Latin characters, in which case the best experience may be to +/// truncate those composing characters with [MaxLengthEnforcement.enforced]. +/// +/// In fields that strictly support only a small subset of characters, such as +/// verification code fields, [MaxLengthEnforcement.enforced] may provide the +/// best experience. +/// {@endtemplate} +/// +/// See also: +/// +/// * [TextField.maxLengthEnforcement] which is used in conjunction with +/// [TextField.maxLength] to limit the length of user input. [TextField] also +/// provides a character counter to provide visual feedback. +enum MaxLengthEnforcement { + /// No enforcement applied to the editing value. It's possible to exceed the + /// max length. + none, + + /// Keep the length of the text input from exceeding the max length even when + /// the text has an unfinished composing region. + enforced, + + /// Users can still input text if the current value is composing even after + /// reaching the max length limit. After composing ends, the value will be + /// truncated. + truncateAfterCompositionEnds, +} + /// A [TextInputFormatter] can be optionally injected into an [EditableText] /// to provide as-you-type validation and formatting of the text being edited. /// @@ -322,8 +365,10 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter { /// /// The [maxLength] must be null, -1 or greater than zero. If it is null or -1 /// then no limit is enforced. - LengthLimitingTextInputFormatter(this.maxLength) - : assert(maxLength == null || maxLength == -1 || maxLength > 0); + LengthLimitingTextInputFormatter( + this.maxLength, { + this.maxLengthEnforcement, + }) : assert(maxLength == null || maxLength == -1 || maxLength > 0); /// The limit on the number of user-perceived characters that this formatter /// will allow. @@ -363,6 +408,47 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter { /// composing is not allowed. final int? maxLength; + /// Determines how the [maxLength] limit should be enforced. + /// + /// Defaults to [MaxLengthEnforcement.enforced]. + /// + /// {@macro flutter.services.textFormatter.maxLengthEnforcement} + final MaxLengthEnforcement? maxLengthEnforcement; + + /// Return an effective [MaxLengthEnforcement] according the target platform. + /// + /// {@template flutter.services.textFormatter.effectiveMaxLengthEnforcement} + /// ### Platform specific behaviors + /// + /// Different platforms follow different behaviors by default, according to + /// their native behavior. + /// * Android, Windows: [MaxLengthEnforcement.enforced]. The native behavior + /// of these platforms is enforced. The composing will be handled by the + /// IME while users are entering CJK characters. + /// * iOS: [MaxLengthEnforcement.truncateAfterCompositionEnds]. iOS has no + /// default behavior and it requires users implement the behavior + /// themselves. Allow the composition to exceed to avoid breaking CJK input. + /// * Web, macOS, linux, fuchsia: + /// [MaxLengthEnforcement.truncateAfterCompositionEnds]. These platforms + /// allow the composition to exceed by default. + /// {@endtemplate} + static MaxLengthEnforcement get inferredDefaultMaxLengthEnforcement { + if (kIsWeb) { + return MaxLengthEnforcement.truncateAfterCompositionEnds; + } else { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.windows: + return MaxLengthEnforcement.enforced; + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.fuchsia: + return MaxLengthEnforcement.truncateAfterCompositionEnds; + } + } + } + /// Truncate the given TextEditingValue to maxLength user-perceived /// characters. /// @@ -376,13 +462,19 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter { iterator.expandNext(maxLength); } final String truncated = iterator.current; + return TextEditingValue( text: truncated, selection: value.selection.copyWith( baseOffset: math.min(value.selection.start, truncated.length), extentOffset: math.min(value.selection.end, truncated.length), ), - composing: TextRange.empty, + composing: !value.composing.isCollapsed && truncated.length > value.composing.start + ? TextRange( + start: value.composing.start, + end: math.min(value.composing.end, truncated.length), + ) + : TextRange.empty, ); } @@ -393,20 +485,43 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter { ) { final int? maxLength = this.maxLength; - if (maxLength == null || maxLength == -1 || newValue.text.characters.length <= maxLength) + if (maxLength == null || + maxLength == -1 || + newValue.text.characters.length <= maxLength) { return newValue; + } assert(maxLength > 0); - // If already at the maximum and tried to enter even more, keep the old - // value. - if (oldValue.text.characters.length == maxLength && !oldValue.composing.isValid) { - return oldValue; - } + switch (maxLengthEnforcement ?? inferredDefaultMaxLengthEnforcement) { + case MaxLengthEnforcement.none: + return newValue; + case MaxLengthEnforcement.enforced: + // If already at the maximum and tried to enter even more, and has no + // selection, keep the old value. + if (oldValue.text.characters.length == maxLength && !oldValue.selection.isValid) { + return oldValue; + } - // Temporarily exempt `newValue` from the maxLength limit if it has a - // composing text going, until the composing is finished. - return newValue.composing.isValid ? newValue : truncate(newValue, maxLength); + // Enforced to return a truncated value. + return truncate(newValue, maxLength); + case MaxLengthEnforcement.truncateAfterCompositionEnds: + // If already at the maximum and tried to enter even more, and the old + // value is not composing, keep the old value. + if (oldValue.text.characters.length == maxLength && + !oldValue.composing.isValid) { + return oldValue; + } + + // Temporarily exempt `newValue` from the maxLength limit if it has a + // composing text going and no enforcement to the composing value, until + // the composing is finished. + if (newValue.composing.isValid) { + return newValue; + } + + return truncate(newValue, maxLength); + } } } diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index a48c4e1c11..771578d973 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -4302,4 +4302,126 @@ void main() { expect(formatters.isEmpty, isTrue); }); + + group('MaxLengthEnforcement', () { + const int maxLength = 5; + + Future setupWidget( + WidgetTester tester, + MaxLengthEnforcement? enforcement, + ) async { + + final Widget widget = CupertinoApp( + home: Center( + child: CupertinoTextField( + maxLength: maxLength, + maxLengthEnforcement: enforcement, + ), + ), + ); + + await tester.pumpWidget(widget); + await tester.pumpAndSettle(); + } + + testWidgets('using none enforcement.', (WidgetTester tester) async { + const MaxLengthEnforcement enforcement = MaxLengthEnforcement.none; + + await setupWidget(tester, enforcement); + + final EditableTextState state = tester.state(find.byType(EditableText)); + + state.updateEditingValue(const TextEditingValue(text: 'abc')); + expect(state.currentTextEditingValue.text, 'abc'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6))); + expect(state.currentTextEditingValue.text, 'abcdef'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6)); + + state.updateEditingValue(const TextEditingValue(text: 'abcdef')); + expect(state.currentTextEditingValue.text, 'abcdef'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + }); + + testWidgets('using enforced.', (WidgetTester tester) async { + const MaxLengthEnforcement enforcement = MaxLengthEnforcement.enforced; + + await setupWidget(tester, enforcement); + + final EditableTextState state = tester.state(find.byType(EditableText)); + + state.updateEditingValue(const TextEditingValue(text: 'abc')); + expect(state.currentTextEditingValue.text, 'abc'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + state.updateEditingValue(const TextEditingValue(text: 'abcde', composing: TextRange(start: 3, end: 5))); + expect(state.currentTextEditingValue.text, 'abcde'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + + state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6))); + expect(state.currentTextEditingValue.text, 'abcde'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + + state.updateEditingValue(const TextEditingValue(text: 'abcdef')); + expect(state.currentTextEditingValue.text, 'abcde'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + }); + + testWidgets('using truncateAfterCompositionEnds.', (WidgetTester tester) async { + const MaxLengthEnforcement enforcement = MaxLengthEnforcement.truncateAfterCompositionEnds; + + await setupWidget(tester, enforcement); + + final EditableTextState state = tester.state(find.byType(EditableText)); + + state.updateEditingValue(const TextEditingValue(text: 'abc')); + expect(state.currentTextEditingValue.text, 'abc'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + state.updateEditingValue(const TextEditingValue(text: 'abcde', composing: TextRange(start: 3, end: 5))); + expect(state.currentTextEditingValue.text, 'abcde'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + + state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6))); + expect(state.currentTextEditingValue.text, 'abcdef'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6)); + + state.updateEditingValue(const TextEditingValue(text: 'abcdef')); + expect(state.currentTextEditingValue.text, 'abcde'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + }); + + testWidgets('using default behavior for different platforms.', (WidgetTester tester) async { + await setupWidget(tester, null); + + final EditableTextState state = tester.state(find.byType(EditableText)); + + state.updateEditingValue(const TextEditingValue(text: '侬好啊')); + expect(state.currentTextEditingValue.text, '侬好啊'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + state.updateEditingValue(const TextEditingValue(text: '侬好啊旁友', composing: TextRange(start: 3, end: 5))); + expect(state.currentTextEditingValue.text, '侬好啊旁友'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + + state.updateEditingValue(const TextEditingValue(text: '侬好啊旁友们', composing: TextRange(start: 3, end: 6))); + if (kIsWeb || + defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS || + defaultTargetPlatform == TargetPlatform.linux || + defaultTargetPlatform == TargetPlatform.fuchsia + ) { + expect(state.currentTextEditingValue.text, '侬好啊旁友们'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6)); + } else { + expect(state.currentTextEditingValue.text, '侬好啊旁友'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + } + + state.updateEditingValue(const TextEditingValue(text: '侬好啊旁友')); + expect(state.currentTextEditingValue.text, '侬好啊旁友'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + }); + }); } diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 39771c8053..917b6e5007 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -2196,7 +2196,7 @@ void main() { TextFormField( key: textFieldKey, maxLength: 3, - maxLengthEnforced: false, + maxLengthEnforcement: MaxLengthEnforcement.none, decoration: InputDecoration( counterText: '', errorText: errorText, @@ -3671,7 +3671,7 @@ void main() { child: TextField( controller: textController, maxLength: 10, - maxLengthEnforced: false, + maxLengthEnforcement: MaxLengthEnforcement.none, ), )); @@ -3688,7 +3688,7 @@ void main() { decoration: const InputDecoration(errorStyle: testStyle), controller: textController, maxLength: 10, - maxLengthEnforced: false, + maxLengthEnforcement: MaxLengthEnforcement.none, ), )); @@ -3718,7 +3718,7 @@ void main() { decoration: const InputDecoration(errorStyle: testStyle), controller: textController, maxLength: 10, - maxLengthEnforced: false, + maxLengthEnforcement: MaxLengthEnforcement.none, ), )); @@ -3748,7 +3748,7 @@ void main() { decoration: const InputDecoration(errorStyle: testStyle), controller: textController, maxLength: 10, - maxLengthEnforced: false, + maxLengthEnforcement: MaxLengthEnforcement.none, ), )); @@ -7504,7 +7504,7 @@ void main() { autocorrect: false, maxLines: 10, maxLength: 100, - maxLengthEnforced: false, + maxLengthEnforcement: MaxLengthEnforcement.none, smartDashesType: SmartDashesType.disabled, smartQuotesType: SmartQuotesType.disabled, enabled: false, @@ -7532,7 +7532,7 @@ void main() { 'smartQuotesType: disabled', 'maxLines: 10', 'maxLength: 100', - 'maxLength not enforced', + 'maxLengthEnforcement: none', 'textInputAction: done', 'textAlign: end', 'textDirection: ltr', @@ -8745,4 +8745,125 @@ void main() { // The label will always float above the content. expect(tester.getTopLeft(find.text('Label')).dy, 12.0); }); + + group('MaxLengthEnforcement', () { + const int maxLength = 5; + + Future setupWidget( + WidgetTester tester, + MaxLengthEnforcement? enforcement, + ) async { + final Widget widget = MaterialApp( + home: Material( + child: TextField( + maxLength: maxLength, + maxLengthEnforcement: enforcement, + ), + ), + ); + + await tester.pumpWidget(widget); + await tester.pumpAndSettle(); + } + + testWidgets('using none enforcement.', (WidgetTester tester) async { + const MaxLengthEnforcement enforcement = MaxLengthEnforcement.none; + + await setupWidget(tester, enforcement); + + final EditableTextState state = tester.state(find.byType(EditableText)); + + state.updateEditingValue(const TextEditingValue(text: 'abc')); + expect(state.currentTextEditingValue.text, 'abc'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6))); + expect(state.currentTextEditingValue.text, 'abcdef'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6)); + + state.updateEditingValue(const TextEditingValue(text: 'abcdef')); + expect(state.currentTextEditingValue.text, 'abcdef'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + }); + + testWidgets('using enforced.', (WidgetTester tester) async { + const MaxLengthEnforcement enforcement = MaxLengthEnforcement.enforced; + + await setupWidget(tester, enforcement); + + final EditableTextState state = tester.state(find.byType(EditableText)); + + state.updateEditingValue(const TextEditingValue(text: 'abc')); + expect(state.currentTextEditingValue.text, 'abc'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + state.updateEditingValue(const TextEditingValue(text: 'abcde', composing: TextRange(start: 3, end: 5))); + expect(state.currentTextEditingValue.text, 'abcde'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + + state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6))); + expect(state.currentTextEditingValue.text, 'abcde'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + + state.updateEditingValue(const TextEditingValue(text: 'abcdef')); + expect(state.currentTextEditingValue.text, 'abcde'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + }); + + testWidgets('using truncateAfterCompositionEnds.', (WidgetTester tester) async { + const MaxLengthEnforcement enforcement = MaxLengthEnforcement.truncateAfterCompositionEnds; + + await setupWidget(tester, enforcement); + + final EditableTextState state = tester.state(find.byType(EditableText)); + + state.updateEditingValue(const TextEditingValue(text: 'abc')); + expect(state.currentTextEditingValue.text, 'abc'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + state.updateEditingValue(const TextEditingValue(text: 'abcde', composing: TextRange(start: 3, end: 5))); + expect(state.currentTextEditingValue.text, 'abcde'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + + state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6))); + expect(state.currentTextEditingValue.text, 'abcdef'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6)); + + state.updateEditingValue(const TextEditingValue(text: 'abcdef')); + expect(state.currentTextEditingValue.text, 'abcde'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + }); + + testWidgets('using default behavior for different platforms.', (WidgetTester tester) async { + await setupWidget(tester, null); + + final EditableTextState state = tester.state(find.byType(EditableText)); + + state.updateEditingValue(const TextEditingValue(text: '侬好啊')); + expect(state.currentTextEditingValue.text, '侬好啊'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + state.updateEditingValue(const TextEditingValue(text: '侬好啊旁友', composing: TextRange(start: 3, end: 5))); + expect(state.currentTextEditingValue.text, '侬好啊旁友'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + + state.updateEditingValue(const TextEditingValue(text: '侬好啊旁友们', composing: TextRange(start: 3, end: 6))); + if (kIsWeb || + defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS || + defaultTargetPlatform == TargetPlatform.linux || + defaultTargetPlatform == TargetPlatform.fuchsia + ) { + expect(state.currentTextEditingValue.text, '侬好啊旁友们'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6)); + } else { + expect(state.currentTextEditingValue.text, '侬好啊旁友'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + } + + state.updateEditingValue(const TextEditingValue(text: '侬好啊旁友')); + expect(state.currentTextEditingValue.text, '侬好啊旁友'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + }); + }); } diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index f2bf2e231e..2ccc1c4a43 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -6500,75 +6500,284 @@ void main() { expect(state.currentTextEditingValue.composing, TextRange.empty); }); - // Regression test for https://github.com/flutter/flutter/issues/65374. - testWidgets('Length formatter will not cause crash while the TextEditingValue is composing', (WidgetTester tester) async { - final TextInputFormatter formatter = LengthLimitingTextInputFormatter(5); - final Widget widget = MaterialApp( - home: EditableText( - backgroundCursorColor: Colors.grey, - controller: controller, - focusNode: focusNode, - inputFormatters: [formatter], - style: textStyle, - cursorColor: cursorColor, - selectionControls: materialTextSelectionControls, - ), - ); + group('Length formatter', () { + const int maxLength = 5; - await tester.pumpWidget(widget); + Future setupWidget( + WidgetTester tester, + LengthLimitingTextInputFormatter formatter, + ) async { + final Widget widget = MaterialApp( + home: EditableText( + backgroundCursorColor: Colors.grey, + controller: controller, + focusNode: focusNode, + inputFormatters: [formatter], + style: textStyle, + cursorColor: cursorColor, + selectionControls: materialTextSelectionControls, + ), + ); - final EditableTextState state = tester.state(find.byType(EditableText)); - state.updateEditingValue(const TextEditingValue(text: '12345')); - expect(state.currentTextEditingValue.composing, TextRange.empty); - state.updateEditingValue(const TextEditingValue(text: '12345', composing: TextRange(start: 2, end: 4))); - expect(state.currentTextEditingValue.composing, const TextRange(start: 2, end: 4)); + await tester.pumpWidget(widget); + await tester.pumpAndSettle(); + } - // Formatter will not update format while the editing value is composing. - state.updateEditingValue(const TextEditingValue(text: '123456', composing: TextRange(start: 2, end: 5))); - expect(state.currentTextEditingValue.text, '123456'); - expect(state.currentTextEditingValue.composing, const TextRange(start: 2, end: 5)); + // Regression test for https://github.com/flutter/flutter/issues/65374. + testWidgets('will not cause crash while the TextEditingValue is composing', (WidgetTester tester) async { + await setupWidget( + tester, + LengthLimitingTextInputFormatter( + maxLength, + maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, + ), + ); - // After composing ends, formatter will update. - state.updateEditingValue(const TextEditingValue(text: '123456')); - expect(state.currentTextEditingValue.text, '12345'); - expect(state.currentTextEditingValue.composing, TextRange.empty); - }); + final EditableTextState state = tester.state(find.byType(EditableText)); + state.updateEditingValue(const TextEditingValue(text: 'abcde')); + expect(state.currentTextEditingValue.composing, TextRange.empty); + state.updateEditingValue(const TextEditingValue(text: 'abcde', composing: TextRange(start: 2, end: 4))); + expect(state.currentTextEditingValue.composing, const TextRange(start: 2, end: 4)); - testWidgets('Length formatter handles composing text correctly, continued', (WidgetTester tester) async { - final TextInputFormatter formatter = LengthLimitingTextInputFormatter(5); - final Widget widget = MaterialApp( - home: EditableText( - backgroundCursorColor: Colors.grey, - controller: controller, - focusNode: focusNode, - inputFormatters: [formatter], - style: textStyle, - cursorColor: cursorColor, - selectionControls: materialTextSelectionControls, - ), - ); + // Formatter will not update format while the editing value is composing. + state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 2, end: 5))); + expect(state.currentTextEditingValue.text, 'abcdef'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 2, end: 5)); - await tester.pumpWidget(widget); - final EditableTextState state = tester.state(find.byType(EditableText)); + // After composing ends, formatter will update. + state.updateEditingValue(const TextEditingValue(text: 'abcdef')); + expect(state.currentTextEditingValue.text, 'abcde'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + }); - // Initially we're at maxLength with no composing text. - controller.text = '12345' ; - assert(state.currentTextEditingValue == const TextEditingValue(text: '12345')); + testWidgets('handles composing text correctly, continued', (WidgetTester tester) async { + await setupWidget( + tester, + LengthLimitingTextInputFormatter( + maxLength, + maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, + ), + ); - // Should be able to change the editing value if the new value is still shorter - // than maxLength. - state.updateEditingValue(const TextEditingValue(text: '12345', composing: TextRange(start: 2, end: 4))); - expect(state.currentTextEditingValue.composing, const TextRange(start: 2, end: 4)); + final EditableTextState state = tester.state(find.byType(EditableText)); - // Reset. - controller.text = '12345' ; - assert(state.currentTextEditingValue == const TextEditingValue(text: '12345')); + // Initially we're at maxLength with no composing text. + controller.text = 'abcde' ; + assert(state.currentTextEditingValue == const TextEditingValue(text: 'abcde')); - // The text should not change when trying to insert when the text is already - // at maxLength. - state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 5, end: 6))); - expect(state.currentTextEditingValue.text, '12345'); - expect(state.currentTextEditingValue.composing, TextRange.empty); + // Should be able to change the editing value if the new value is still shorter + // than maxLength. + state.updateEditingValue(const TextEditingValue(text: 'abcde', composing: TextRange(start: 2, end: 4))); + expect(state.currentTextEditingValue.composing, const TextRange(start: 2, end: 4)); + + // Reset. + controller.text = 'abcde' ; + assert(state.currentTextEditingValue == const TextEditingValue(text: 'abcde')); + + // The text should not change when trying to insert when the text is already + // at maxLength. + state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 5, end: 6))); + expect(state.currentTextEditingValue.text, 'abcde'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + }); + + // Regression test for https://github.com/flutter/flutter/issues/68086. + testWidgets('enforced composing truncated', (WidgetTester tester) async { + await setupWidget( + tester, + LengthLimitingTextInputFormatter( + maxLength, + maxLengthEnforcement: MaxLengthEnforcement.enforced, + ), + ); + + final EditableTextState state = tester.state(find.byType(EditableText)); + + // Initially we're at maxLength with no composing text. + state.updateEditingValue(const TextEditingValue(text: 'abcde')); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + // When it's not longer than `maxLength`, it can still start composing. + state.updateEditingValue(const TextEditingValue(text: 'abcde', composing: TextRange(start: 3, end: 5))); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + + // `newValue` will be truncated if `composingMaxLengthEnforced`. + state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 3, end: 6))); + expect(state.currentTextEditingValue.text, 'abcde'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + + // Reset the value. + state.updateEditingValue(const TextEditingValue(text: 'abcde')); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + // Change the value in order to take effects on web test. + state.updateEditingValue(const TextEditingValue(text: '你好啊朋友')); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + // Start composing with a longer value, it should be the same state. + state.updateEditingValue(const TextEditingValue(text: '你好啊朋友们', composing: TextRange(start: 3, end: 6))); + expect(state.currentTextEditingValue.composing, TextRange.empty); + }); + + // Regression test for https://github.com/flutter/flutter/issues/68086. + testWidgets('default truncate behaviors with different platforms', (WidgetTester tester) async { + await setupWidget(tester, LengthLimitingTextInputFormatter(maxLength)); + + final EditableTextState state = tester.state(find.byType(EditableText)); + + // Initially we're at maxLength with no composing text. + state.updateEditingValue(const TextEditingValue(text: '你好啊朋友')); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + // When it's not longer than `maxLength`, it can still start composing. + state.updateEditingValue(const TextEditingValue(text: '你好啊朋友', composing: TextRange(start: 3, end: 5))); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + + state.updateEditingValue(const TextEditingValue(text: '你好啊朋友们', composing: TextRange(start: 3, end: 6))); + if (kIsWeb || + defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS || + defaultTargetPlatform == TargetPlatform.linux || + defaultTargetPlatform == TargetPlatform.fuchsia + ) { + // `newValue` will will not be truncated on couple platforms. + expect(state.currentTextEditingValue.text, '你好啊朋友们'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 6)); + } else { + // `newValue` on other platforms will be truncated. + expect(state.currentTextEditingValue.text, '你好啊朋友'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + } + + // Reset the value. + state.updateEditingValue(const TextEditingValue(text: '你好啊朋友')); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + // Start composing with a longer value, it should be the same state. + state.updateEditingValue(const TextEditingValue(text: '你好啊朋友们', composing: TextRange(start: 3, end: 6))); + expect(state.currentTextEditingValue.text, '你好啊朋友'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + }); + + // Regression test for https://github.com/flutter/flutter/issues/68086. + testWidgets('composing range removed if it\'s overflowed the truncated value\'s length', (WidgetTester tester) async { + await setupWidget( + tester, + LengthLimitingTextInputFormatter( + maxLength, + maxLengthEnforcement: MaxLengthEnforcement.enforced, + ), + ); + + final EditableTextState state = tester.state(find.byType(EditableText)); + + // Initially we're not at maxLength with no composing text. + state.updateEditingValue(const TextEditingValue(text: 'abc')); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + // Start composing. + state.updateEditingValue(const TextEditingValue(text: 'abcde', composing: TextRange(start: 3, end: 5))); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + + // Reset the value. + state.updateEditingValue(const TextEditingValue(text: 'abc')); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + // Start composing with a range already overflowed the truncated length. + state.updateEditingValue(const TextEditingValue(text: 'abcdefgh', composing: TextRange(start: 5, end: 7))); + expect(state.currentTextEditingValue.composing, TextRange.empty); + }); + + // Regression test for https://github.com/flutter/flutter/issues/68086. + testWidgets('composing range removed with different platforms', (WidgetTester tester) async { + await setupWidget(tester, LengthLimitingTextInputFormatter(maxLength)); + + final EditableTextState state = tester.state(find.byType(EditableText)); + + // Initially we're not at maxLength with no composing text. + state.updateEditingValue(const TextEditingValue(text: 'abc')); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + // Start composing. + state.updateEditingValue(const TextEditingValue(text: 'abcde', composing: TextRange(start: 3, end: 5))); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + + // Reset the value. + state.updateEditingValue(const TextEditingValue(text: 'abc')); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + // Start composing with a range already overflowed the truncated length. + state.updateEditingValue(const TextEditingValue(text: 'abcdefgh', composing: TextRange(start: 5, end: 7))); + if (kIsWeb || + defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS || + defaultTargetPlatform == TargetPlatform.linux || + defaultTargetPlatform == TargetPlatform.fuchsia + ) { + expect(state.currentTextEditingValue.composing, const TextRange(start: 5, end: 7)); + } else { + expect(state.currentTextEditingValue.composing, TextRange.empty); + } + }); + + testWidgets('composing range handled correctly when it\'s overflowed', (WidgetTester tester) async { + const String string = '👨‍👩‍👦0123456'; + + await setupWidget(tester, LengthLimitingTextInputFormatter(maxLength)); + + final EditableTextState state = tester.state(find.byType(EditableText)); + + // Initially we're not at maxLength with no composing text. + state.updateEditingValue(const TextEditingValue(text: string)); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + // Clearing composing range if collapsed. + state.updateEditingValue(const TextEditingValue(text: string, composing: TextRange(start: 10, end: 10))); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + // Clearing composing range if overflowed. + state.updateEditingValue(const TextEditingValue(text: string, composing: TextRange(start: 10, end: 11))); + expect(state.currentTextEditingValue.composing, TextRange.empty); + }); + + // Regression test for https://github.com/flutter/flutter/issues/68086. + testWidgets('typing in the middle with different platforms.', (WidgetTester tester) async { + await setupWidget(tester, LengthLimitingTextInputFormatter(maxLength)); + + final EditableTextState state = tester.state(find.byType(EditableText)); + + // Initially we're not at maxLength with no composing text. + state.updateEditingValue(const TextEditingValue(text: 'abc')); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + // Start typing in the middle. + state.updateEditingValue(const TextEditingValue(text: 'abDEc', composing: TextRange(start: 3, end: 4))); + expect(state.currentTextEditingValue.text, 'abDEc'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 4)); + + // Keep typing when the value has exceed the limitation. + state.updateEditingValue(const TextEditingValue(text: 'abDEFc', composing: TextRange(start: 3, end: 5))); + if (kIsWeb || + defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS || + defaultTargetPlatform == TargetPlatform.linux || + defaultTargetPlatform == TargetPlatform.fuchsia + ) { + expect(state.currentTextEditingValue.text, 'abDEFc'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 5)); + } else { + expect(state.currentTextEditingValue.text, 'abDEc'); + expect(state.currentTextEditingValue.composing, const TextRange(start: 3, end: 4)); + } + + // Reset the value according to the limit. + state.updateEditingValue(const TextEditingValue(text: 'abDEc')); + expect(state.currentTextEditingValue.text, 'abDEc'); + expect(state.currentTextEditingValue.composing, TextRange.empty); + + state.updateEditingValue(const TextEditingValue(text: 'abDEFc', composing: TextRange(start: 4, end: 5))); + expect(state.currentTextEditingValue.composing, TextRange.empty); + }); }); group('callback errors', () { diff --git a/packages/flutter/test/widgets/form_test.dart b/packages/flutter/test/widgets/form_test.dart index c1d57d7488..7274fc86df 100644 --- a/packages/flutter/test/widgets/form_test.dart +++ b/packages/flutter/test/widgets/form_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; void main() { testWidgets('onSaved callback is called', (WidgetTester tester) async { @@ -848,7 +849,7 @@ void main() { expect(() => builder(), throwsAssertionError); }); - // Regression test for https://github.com/flutter/flutter/issues/65374. + // Regression test for https://github.com/flutter/flutter/issues/63753. testWidgets('Validate form should return correct validation if the value is composing', (WidgetTester tester) async { final GlobalKey formKey = GlobalKey(); String? fieldValue; @@ -864,6 +865,7 @@ void main() { key: formKey, child: TextFormField( maxLength: 5, + maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, onSaved: (String? value) { fieldValue = value; }, validator: (String? value) => (value != null && value.length > 5) ? 'Exceeded' : null, ),