Introduce MaxLengthEnforcement
(#68086)
This commit is contained in:
parent
635dfc3e4f
commit
135a8c22b1
@ -260,6 +260,7 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
this.expands = false,
|
this.expands = false,
|
||||||
this.maxLength,
|
this.maxLength,
|
||||||
this.maxLengthEnforced = true,
|
this.maxLengthEnforced = true,
|
||||||
|
this.maxLengthEnforcement,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.onEditingComplete,
|
this.onEditingComplete,
|
||||||
this.onSubmitted,
|
this.onSubmitted,
|
||||||
@ -292,6 +293,10 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
||||||
assert(enableSuggestions != null),
|
assert(enableSuggestions != null),
|
||||||
assert(maxLengthEnforced != null),
|
assert(maxLengthEnforced != null),
|
||||||
|
assert(
|
||||||
|
maxLengthEnforced || maxLengthEnforcement == null,
|
||||||
|
'maxLengthEnforced is deprecated, use only maxLengthEnforcement',
|
||||||
|
),
|
||||||
assert(scrollPadding != null),
|
assert(scrollPadding != null),
|
||||||
assert(dragStartBehavior != null),
|
assert(dragStartBehavior != null),
|
||||||
assert(selectionHeightStyle != null),
|
assert(selectionHeightStyle != null),
|
||||||
@ -402,6 +407,7 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
this.expands = false,
|
this.expands = false,
|
||||||
this.maxLength,
|
this.maxLength,
|
||||||
this.maxLengthEnforced = true,
|
this.maxLengthEnforced = true,
|
||||||
|
this.maxLengthEnforcement,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.onEditingComplete,
|
this.onEditingComplete,
|
||||||
this.onSubmitted,
|
this.onSubmitted,
|
||||||
@ -434,6 +440,10 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
||||||
assert(enableSuggestions != null),
|
assert(enableSuggestions != null),
|
||||||
assert(maxLengthEnforced != null),
|
assert(maxLengthEnforced != null),
|
||||||
|
assert(
|
||||||
|
maxLengthEnforced || maxLengthEnforcement == null,
|
||||||
|
'maxLengthEnforced is deprecated, use only maxLengthEnforcement',
|
||||||
|
),
|
||||||
assert(scrollPadding != null),
|
assert(scrollPadding != null),
|
||||||
assert(dragStartBehavior != null),
|
assert(dragStartBehavior != null),
|
||||||
assert(selectionHeightStyle != null),
|
assert(selectionHeightStyle != null),
|
||||||
@ -619,12 +629,13 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
/// The maximum number of characters (Unicode scalar values) to allow in the
|
/// The maximum number of characters (Unicode scalar values) to allow in the
|
||||||
/// text field.
|
/// text field.
|
||||||
///
|
///
|
||||||
/// If set, a character counter will be displayed below the
|
/// After [maxLength] characters have been input, additional input
|
||||||
/// field, showing how many characters have been entered and how many are
|
/// is ignored, unless [maxLengthEnforcement] is set to
|
||||||
/// allowed. After [maxLength] characters have been input, additional input
|
/// [MaxLengthEnforcement.none].
|
||||||
/// is ignored, unless [maxLengthEnforced] is set to false. The TextField
|
///
|
||||||
/// enforces the length with a [LengthLimitingTextInputFormatter], which is
|
/// The TextField enforces the length with a
|
||||||
/// evaluated after the supplied [inputFormatters], if any.
|
/// [LengthLimitingTextInputFormatter], which is evaluated after the supplied
|
||||||
|
/// [inputFormatters], if any.
|
||||||
///
|
///
|
||||||
/// This value must be either null or greater than zero. If set to null
|
/// 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.
|
/// (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}
|
/// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength}
|
||||||
final int? 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]
|
/// If true, prevents the field from allowing more than [maxLength]
|
||||||
/// characters.
|
/// 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;
|
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}
|
/// {@macro flutter.widgets.editableText.onChanged}
|
||||||
final ValueChanged<String>? onChanged;
|
final ValueChanged<String>? onChanged;
|
||||||
|
|
||||||
@ -761,6 +781,7 @@ class CupertinoTextField extends StatefulWidget {
|
|||||||
properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
|
properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
|
||||||
properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
|
properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
|
||||||
properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, ifTrue: 'max length enforced'));
|
properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, ifTrue: 'max length enforced'));
|
||||||
|
properties.add(EnumProperty<MaxLengthEnforcement>('maxLengthEnforcement', maxLengthEnforcement, defaultValue: null));
|
||||||
properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
|
properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
|
||||||
properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
|
properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
|
||||||
properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
|
properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
|
||||||
@ -783,6 +804,9 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
|
|||||||
FocusNode? _focusNode;
|
FocusNode? _focusNode;
|
||||||
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
|
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
|
||||||
|
|
||||||
|
MaxLengthEnforcement get _effectiveMaxLengthEnforcement => widget.maxLengthEnforcement
|
||||||
|
?? LengthLimitingTextInputFormatter.inferredDefaultMaxLengthEnforcement;
|
||||||
|
|
||||||
bool _showSelectionHandles = false;
|
bool _showSelectionHandles = false;
|
||||||
|
|
||||||
late _CupertinoTextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
|
late _CupertinoTextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
|
||||||
@ -1030,7 +1054,11 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
|
|||||||
final Offset cursorOffset = Offset(_iOSHorizontalCursorOffsetPixels / MediaQuery.of(context).devicePixelRatio, 0);
|
final Offset cursorOffset = Offset(_iOSHorizontalCursorOffsetPixels / MediaQuery.of(context).devicePixelRatio, 0);
|
||||||
final List<TextInputFormatter> formatters = <TextInputFormatter>[
|
final List<TextInputFormatter> formatters = <TextInputFormatter>[
|
||||||
...?widget.inputFormatters,
|
...?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);
|
final CupertinoThemeData themeData = CupertinoTheme.of(context);
|
||||||
|
|
||||||
|
@ -302,10 +302,11 @@ class TextField extends StatefulWidget {
|
|||||||
/// [TextField.noMaxLength] then only the current length is displayed.
|
/// [TextField.noMaxLength] then only the current length is displayed.
|
||||||
///
|
///
|
||||||
/// After [maxLength] characters have been input, additional input
|
/// After [maxLength] characters have been input, additional input
|
||||||
/// is ignored, unless [maxLengthEnforced] is set to false. The text field
|
/// is ignored, unless [maxLengthEnforcement] is set to
|
||||||
/// enforces the length with a [LengthLimitingTextInputFormatter], which is
|
/// [MaxLengthEnforcement.none].
|
||||||
/// evaluated after the supplied [inputFormatters], if any. The [maxLength]
|
/// The text field enforces the length with a [LengthLimitingTextInputFormatter],
|
||||||
/// value must be either null or greater than zero.
|
/// 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]
|
/// If [maxLengthEnforced] is set to false, then more than [maxLength]
|
||||||
/// characters may be entered, and the error counter and divider will
|
/// characters may be entered, and the error counter and divider will
|
||||||
@ -356,6 +357,7 @@ class TextField extends StatefulWidget {
|
|||||||
this.expands = false,
|
this.expands = false,
|
||||||
this.maxLength,
|
this.maxLength,
|
||||||
this.maxLengthEnforced = true,
|
this.maxLengthEnforced = true,
|
||||||
|
this.maxLengthEnforcement,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.onEditingComplete,
|
this.onEditingComplete,
|
||||||
this.onSubmitted,
|
this.onSubmitted,
|
||||||
@ -391,6 +393,10 @@ class TextField extends StatefulWidget {
|
|||||||
assert(enableSuggestions != null),
|
assert(enableSuggestions != null),
|
||||||
assert(enableInteractiveSelection != null),
|
assert(enableInteractiveSelection != null),
|
||||||
assert(maxLengthEnforced != null),
|
assert(maxLengthEnforced != null),
|
||||||
|
assert(
|
||||||
|
maxLengthEnforced || maxLengthEnforcement == null,
|
||||||
|
'maxLengthEnforced is deprecated, use only maxLengthEnforcement',
|
||||||
|
),
|
||||||
assert(scrollPadding != null),
|
assert(scrollPadding != null),
|
||||||
assert(dragStartBehavior != null),
|
assert(dragStartBehavior != null),
|
||||||
assert(selectionHeightStyle != null),
|
assert(selectionHeightStyle != null),
|
||||||
@ -568,9 +574,11 @@ class TextField extends StatefulWidget {
|
|||||||
/// to [TextField.noMaxLength] then only the current character count is displayed.
|
/// to [TextField.noMaxLength] then only the current character count is displayed.
|
||||||
///
|
///
|
||||||
/// After [maxLength] characters have been input, additional input
|
/// After [maxLength] characters have been input, additional input
|
||||||
/// is ignored, unless [maxLengthEnforced] is set to false. The text field
|
/// is ignored, unless [maxLengthEnforcement] is set to
|
||||||
/// enforces the length with a [LengthLimitingTextInputFormatter], which is
|
/// [MaxLengthEnforcement.none].
|
||||||
/// evaluated after the supplied [inputFormatters], if any.
|
///
|
||||||
|
/// 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.
|
/// 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
|
/// 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
|
/// Whitespace characters (e.g. newline, space, tab) are included in the
|
||||||
/// character count.
|
/// 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
|
/// characters may be entered, but the error counter and divider will switch
|
||||||
/// to the [decoration]'s [InputDecoration.errorStyle] when the limit is
|
/// to the [decoration]'s [InputDecoration.errorStyle] when the limit is
|
||||||
/// exceeded.
|
/// exceeded.
|
||||||
@ -588,14 +597,21 @@ class TextField extends StatefulWidget {
|
|||||||
/// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength}
|
/// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength}
|
||||||
final int? 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
|
/// If [maxLength] is set, [maxLengthEnforced] indicates whether or not to
|
||||||
/// enforce the limit, or merely provide a character counter and warning when
|
/// enforce the limit, or merely provide a character counter and warning when
|
||||||
/// [maxLength] is exceeded.
|
/// [maxLength] is exceeded.
|
||||||
|
///
|
||||||
|
/// If true, prevents the field from allowing more than [maxLength]
|
||||||
|
/// characters.
|
||||||
final bool maxLengthEnforced;
|
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}
|
/// {@macro flutter.widgets.editableText.onChanged}
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
@ -810,6 +826,7 @@ class TextField extends StatefulWidget {
|
|||||||
properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
|
properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
|
||||||
properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
|
properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
|
||||||
properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, defaultValue: true, ifFalse: 'maxLength not enforced'));
|
properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, defaultValue: true, ifFalse: 'maxLength not enforced'));
|
||||||
|
properties.add(EnumProperty<MaxLengthEnforcement>('maxLengthEnforcement', maxLengthEnforcement, defaultValue: null));
|
||||||
properties.add(EnumProperty<TextInputAction>('textInputAction', textInputAction, defaultValue: null));
|
properties.add(EnumProperty<TextInputAction>('textInputAction', textInputAction, defaultValue: null));
|
||||||
properties.add(EnumProperty<TextCapitalization>('textCapitalization', textCapitalization, defaultValue: TextCapitalization.none));
|
properties.add(EnumProperty<TextCapitalization>('textCapitalization', textCapitalization, defaultValue: TextCapitalization.none));
|
||||||
properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: TextAlign.start));
|
properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: TextAlign.start));
|
||||||
@ -835,6 +852,9 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
|||||||
FocusNode? _focusNode;
|
FocusNode? _focusNode;
|
||||||
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
|
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
|
||||||
|
|
||||||
|
MaxLengthEnforcement get _effectiveMaxLengthEnforcement => widget.maxLengthEnforcement
|
||||||
|
?? LengthLimitingTextInputFormatter.inferredDefaultMaxLengthEnforcement;
|
||||||
|
|
||||||
bool _isHovering = false;
|
bool _isHovering = false;
|
||||||
|
|
||||||
bool get needsCounter => widget.maxLength != null
|
bool get needsCounter => widget.maxLength != null
|
||||||
@ -1095,7 +1115,11 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
|||||||
final FocusNode focusNode = _effectiveFocusNode;
|
final FocusNode focusNode = _effectiveFocusNode;
|
||||||
final List<TextInputFormatter> formatters = <TextInputFormatter>[
|
final List<TextInputFormatter> formatters = <TextInputFormatter>[
|
||||||
...?widget.inputFormatters,
|
...?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;
|
TextSelectionControls? textSelectionControls = widget.selectionControls;
|
||||||
@ -1226,6 +1250,16 @@ class _TextFieldState extends State<TextField> 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(
|
return MouseRegion(
|
||||||
cursor: effectiveMouseCursor,
|
cursor: effectiveMouseCursor,
|
||||||
onEnter: (PointerEnterEvent event) => _handleHover(true),
|
onEnter: (PointerEnterEvent event) => _handleHover(true),
|
||||||
@ -1236,9 +1270,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
|||||||
animation: controller, // changes the _currentLength
|
animation: controller, // changes the _currentLength
|
||||||
builder: (BuildContext context, Widget? child) {
|
builder: (BuildContext context, Widget? child) {
|
||||||
return Semantics(
|
return Semantics(
|
||||||
maxValueLength: widget.maxLengthEnforced && widget.maxLength != null && widget.maxLength! > 0
|
maxValueLength: semanticsMaxValueLength,
|
||||||
? widget.maxLength
|
|
||||||
: null,
|
|
||||||
currentValueLength: _currentLength,
|
currentValueLength: _currentLength,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (!_effectiveController.selection.isValid)
|
if (!_effectiveController.selection.isValid)
|
||||||
|
@ -163,6 +163,7 @@ class TextFormField extends FormField<String> {
|
|||||||
)
|
)
|
||||||
bool autovalidate = false,
|
bool autovalidate = false,
|
||||||
bool maxLengthEnforced = true,
|
bool maxLengthEnforced = true,
|
||||||
|
MaxLengthEnforcement? maxLengthEnforcement,
|
||||||
int? maxLines = 1,
|
int? maxLines = 1,
|
||||||
int? minLines,
|
int? minLines,
|
||||||
bool expands = false,
|
bool expands = false,
|
||||||
@ -202,6 +203,10 @@ class TextFormField extends FormField<String> {
|
|||||||
'autovalidate and autovalidateMode should not be used together.'
|
'autovalidate and autovalidateMode should not be used together.'
|
||||||
),
|
),
|
||||||
assert(maxLengthEnforced != null),
|
assert(maxLengthEnforced != null),
|
||||||
|
assert(
|
||||||
|
maxLengthEnforced || maxLengthEnforcement == null,
|
||||||
|
'maxLengthEnforced is deprecated, use only maxLengthEnforcement',
|
||||||
|
),
|
||||||
assert(scrollPadding != null),
|
assert(scrollPadding != null),
|
||||||
assert(maxLines == null || maxLines > 0),
|
assert(maxLines == null || maxLines > 0),
|
||||||
assert(minLines == null || minLines > 0),
|
assert(minLines == null || minLines > 0),
|
||||||
@ -259,6 +264,7 @@ class TextFormField extends FormField<String> {
|
|||||||
smartQuotesType: smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
smartQuotesType: smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
||||||
enableSuggestions: enableSuggestions,
|
enableSuggestions: enableSuggestions,
|
||||||
maxLengthEnforced: maxLengthEnforced,
|
maxLengthEnforced: maxLengthEnforced,
|
||||||
|
maxLengthEnforcement: maxLengthEnforcement,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
minLines: minLines,
|
minLines: minLines,
|
||||||
expands: expands,
|
expands: expands,
|
||||||
|
@ -11,6 +11,49 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'text_editing.dart';
|
import 'text_editing.dart';
|
||||||
import 'text_input.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]
|
/// A [TextInputFormatter] can be optionally injected into an [EditableText]
|
||||||
/// to provide as-you-type validation and formatting of the text being edited.
|
/// 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
|
/// The [maxLength] must be null, -1 or greater than zero. If it is null or -1
|
||||||
/// then no limit is enforced.
|
/// then no limit is enforced.
|
||||||
LengthLimitingTextInputFormatter(this.maxLength)
|
LengthLimitingTextInputFormatter(
|
||||||
: assert(maxLength == null || maxLength == -1 || maxLength > 0);
|
this.maxLength, {
|
||||||
|
this.maxLengthEnforcement,
|
||||||
|
}) : assert(maxLength == null || maxLength == -1 || maxLength > 0);
|
||||||
|
|
||||||
/// The limit on the number of user-perceived characters that this formatter
|
/// The limit on the number of user-perceived characters that this formatter
|
||||||
/// will allow.
|
/// will allow.
|
||||||
@ -363,6 +408,47 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter {
|
|||||||
/// composing is not allowed.
|
/// composing is not allowed.
|
||||||
final int? maxLength;
|
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
|
/// Truncate the given TextEditingValue to maxLength user-perceived
|
||||||
/// characters.
|
/// characters.
|
||||||
///
|
///
|
||||||
@ -376,13 +462,19 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter {
|
|||||||
iterator.expandNext(maxLength);
|
iterator.expandNext(maxLength);
|
||||||
}
|
}
|
||||||
final String truncated = iterator.current;
|
final String truncated = iterator.current;
|
||||||
|
|
||||||
return TextEditingValue(
|
return TextEditingValue(
|
||||||
text: truncated,
|
text: truncated,
|
||||||
selection: value.selection.copyWith(
|
selection: value.selection.copyWith(
|
||||||
baseOffset: math.min(value.selection.start, truncated.length),
|
baseOffset: math.min(value.selection.start, truncated.length),
|
||||||
extentOffset: math.min(value.selection.end, 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;
|
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;
|
return newValue;
|
||||||
|
}
|
||||||
|
|
||||||
assert(maxLength > 0);
|
assert(maxLength > 0);
|
||||||
|
|
||||||
// If already at the maximum and tried to enter even more, keep the old
|
switch (maxLengthEnforcement ?? inferredDefaultMaxLengthEnforcement) {
|
||||||
// value.
|
case MaxLengthEnforcement.none:
|
||||||
if (oldValue.text.characters.length == maxLength && !oldValue.composing.isValid) {
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
return oldValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporarily exempt `newValue` from the maxLength limit if it has a
|
// Temporarily exempt `newValue` from the maxLength limit if it has a
|
||||||
// composing text going, until the composing is finished.
|
// composing text going and no enforcement to the composing value, until
|
||||||
return newValue.composing.isValid ? newValue : truncate(newValue, maxLength);
|
// the composing is finished.
|
||||||
|
if (newValue.composing.isValid) {
|
||||||
|
return newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return truncate(newValue, maxLength);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4302,4 +4302,126 @@ void main() {
|
|||||||
|
|
||||||
expect(formatters.isEmpty, isTrue);
|
expect(formatters.isEmpty, isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('MaxLengthEnforcement', () {
|
||||||
|
const int maxLength = 5;
|
||||||
|
|
||||||
|
Future<void> 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -2196,7 +2196,7 @@ void main() {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
key: textFieldKey,
|
key: textFieldKey,
|
||||||
maxLength: 3,
|
maxLength: 3,
|
||||||
maxLengthEnforced: false,
|
maxLengthEnforcement: MaxLengthEnforcement.none,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
counterText: '',
|
counterText: '',
|
||||||
errorText: errorText,
|
errorText: errorText,
|
||||||
@ -3671,7 +3671,7 @@ void main() {
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
controller: textController,
|
controller: textController,
|
||||||
maxLength: 10,
|
maxLength: 10,
|
||||||
maxLengthEnforced: false,
|
maxLengthEnforcement: MaxLengthEnforcement.none,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -3688,7 +3688,7 @@ void main() {
|
|||||||
decoration: const InputDecoration(errorStyle: testStyle),
|
decoration: const InputDecoration(errorStyle: testStyle),
|
||||||
controller: textController,
|
controller: textController,
|
||||||
maxLength: 10,
|
maxLength: 10,
|
||||||
maxLengthEnforced: false,
|
maxLengthEnforcement: MaxLengthEnforcement.none,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -3718,7 +3718,7 @@ void main() {
|
|||||||
decoration: const InputDecoration(errorStyle: testStyle),
|
decoration: const InputDecoration(errorStyle: testStyle),
|
||||||
controller: textController,
|
controller: textController,
|
||||||
maxLength: 10,
|
maxLength: 10,
|
||||||
maxLengthEnforced: false,
|
maxLengthEnforcement: MaxLengthEnforcement.none,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -3748,7 +3748,7 @@ void main() {
|
|||||||
decoration: const InputDecoration(errorStyle: testStyle),
|
decoration: const InputDecoration(errorStyle: testStyle),
|
||||||
controller: textController,
|
controller: textController,
|
||||||
maxLength: 10,
|
maxLength: 10,
|
||||||
maxLengthEnforced: false,
|
maxLengthEnforcement: MaxLengthEnforcement.none,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -7504,7 +7504,7 @@ void main() {
|
|||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
maxLines: 10,
|
maxLines: 10,
|
||||||
maxLength: 100,
|
maxLength: 100,
|
||||||
maxLengthEnforced: false,
|
maxLengthEnforcement: MaxLengthEnforcement.none,
|
||||||
smartDashesType: SmartDashesType.disabled,
|
smartDashesType: SmartDashesType.disabled,
|
||||||
smartQuotesType: SmartQuotesType.disabled,
|
smartQuotesType: SmartQuotesType.disabled,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -7532,7 +7532,7 @@ void main() {
|
|||||||
'smartQuotesType: disabled',
|
'smartQuotesType: disabled',
|
||||||
'maxLines: 10',
|
'maxLines: 10',
|
||||||
'maxLength: 100',
|
'maxLength: 100',
|
||||||
'maxLength not enforced',
|
'maxLengthEnforcement: none',
|
||||||
'textInputAction: done',
|
'textInputAction: done',
|
||||||
'textAlign: end',
|
'textAlign: end',
|
||||||
'textDirection: ltr',
|
'textDirection: ltr',
|
||||||
@ -8745,4 +8745,125 @@ void main() {
|
|||||||
// The label will always float above the content.
|
// The label will always float above the content.
|
||||||
expect(tester.getTopLeft(find.text('Label')).dy, 12.0);
|
expect(tester.getTopLeft(find.text('Label')).dy, 12.0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('MaxLengthEnforcement', () {
|
||||||
|
const int maxLength = 5;
|
||||||
|
|
||||||
|
Future<void> 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -6500,9 +6500,13 @@ void main() {
|
|||||||
expect(state.currentTextEditingValue.composing, TextRange.empty);
|
expect(state.currentTextEditingValue.composing, TextRange.empty);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Regression test for https://github.com/flutter/flutter/issues/65374.
|
group('Length formatter', () {
|
||||||
testWidgets('Length formatter will not cause crash while the TextEditingValue is composing', (WidgetTester tester) async {
|
const int maxLength = 5;
|
||||||
final TextInputFormatter formatter = LengthLimitingTextInputFormatter(5);
|
|
||||||
|
Future<void> setupWidget(
|
||||||
|
WidgetTester tester,
|
||||||
|
LengthLimitingTextInputFormatter formatter,
|
||||||
|
) async {
|
||||||
final Widget widget = MaterialApp(
|
final Widget widget = MaterialApp(
|
||||||
home: EditableText(
|
home: EditableText(
|
||||||
backgroundCursorColor: Colors.grey,
|
backgroundCursorColor: Colors.grey,
|
||||||
@ -6516,61 +6520,266 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(widget);
|
await tester.pumpWidget(widget);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||||||
state.updateEditingValue(const TextEditingValue(text: '12345'));
|
state.updateEditingValue(const TextEditingValue(text: 'abcde'));
|
||||||
expect(state.currentTextEditingValue.composing, TextRange.empty);
|
expect(state.currentTextEditingValue.composing, TextRange.empty);
|
||||||
state.updateEditingValue(const TextEditingValue(text: '12345', composing: TextRange(start: 2, end: 4)));
|
state.updateEditingValue(const TextEditingValue(text: 'abcde', composing: TextRange(start: 2, end: 4)));
|
||||||
expect(state.currentTextEditingValue.composing, const TextRange(start: 2, end: 4));
|
expect(state.currentTextEditingValue.composing, const TextRange(start: 2, end: 4));
|
||||||
|
|
||||||
// Formatter will not update format while the editing value is composing.
|
// Formatter will not update format while the editing value is composing.
|
||||||
state.updateEditingValue(const TextEditingValue(text: '123456', composing: TextRange(start: 2, end: 5)));
|
state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 2, end: 5)));
|
||||||
expect(state.currentTextEditingValue.text, '123456');
|
expect(state.currentTextEditingValue.text, 'abcdef');
|
||||||
expect(state.currentTextEditingValue.composing, const TextRange(start: 2, end: 5));
|
expect(state.currentTextEditingValue.composing, const TextRange(start: 2, end: 5));
|
||||||
|
|
||||||
// After composing ends, formatter will update.
|
// After composing ends, formatter will update.
|
||||||
state.updateEditingValue(const TextEditingValue(text: '123456'));
|
state.updateEditingValue(const TextEditingValue(text: 'abcdef'));
|
||||||
expect(state.currentTextEditingValue.text, '12345');
|
expect(state.currentTextEditingValue.text, 'abcde');
|
||||||
expect(state.currentTextEditingValue.composing, TextRange.empty);
|
expect(state.currentTextEditingValue.composing, TextRange.empty);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Length formatter handles composing text correctly, continued', (WidgetTester tester) async {
|
testWidgets('handles composing text correctly, continued', (WidgetTester tester) async {
|
||||||
final TextInputFormatter formatter = LengthLimitingTextInputFormatter(5);
|
await setupWidget(
|
||||||
final Widget widget = MaterialApp(
|
tester,
|
||||||
home: EditableText(
|
LengthLimitingTextInputFormatter(
|
||||||
backgroundCursorColor: Colors.grey,
|
maxLength,
|
||||||
controller: controller,
|
maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
|
||||||
focusNode: focusNode,
|
|
||||||
inputFormatters: <TextInputFormatter>[formatter],
|
|
||||||
style: textStyle,
|
|
||||||
cursorColor: cursorColor,
|
|
||||||
selectionControls: materialTextSelectionControls,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(widget);
|
|
||||||
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||||||
|
|
||||||
// Initially we're at maxLength with no composing text.
|
// Initially we're at maxLength with no composing text.
|
||||||
controller.text = '12345' ;
|
controller.text = 'abcde' ;
|
||||||
assert(state.currentTextEditingValue == const TextEditingValue(text: '12345'));
|
assert(state.currentTextEditingValue == const TextEditingValue(text: 'abcde'));
|
||||||
|
|
||||||
// Should be able to change the editing value if the new value is still shorter
|
// Should be able to change the editing value if the new value is still shorter
|
||||||
// than maxLength.
|
// than maxLength.
|
||||||
state.updateEditingValue(const TextEditingValue(text: '12345', composing: TextRange(start: 2, end: 4)));
|
state.updateEditingValue(const TextEditingValue(text: 'abcde', composing: TextRange(start: 2, end: 4)));
|
||||||
expect(state.currentTextEditingValue.composing, const TextRange(start: 2, end: 4));
|
expect(state.currentTextEditingValue.composing, const TextRange(start: 2, end: 4));
|
||||||
|
|
||||||
// Reset.
|
// Reset.
|
||||||
controller.text = '12345' ;
|
controller.text = 'abcde' ;
|
||||||
assert(state.currentTextEditingValue == const TextEditingValue(text: '12345'));
|
assert(state.currentTextEditingValue == const TextEditingValue(text: 'abcde'));
|
||||||
|
|
||||||
// The text should not change when trying to insert when the text is already
|
// The text should not change when trying to insert when the text is already
|
||||||
// at maxLength.
|
// at maxLength.
|
||||||
state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 5, end: 6)));
|
state.updateEditingValue(const TextEditingValue(text: 'abcdef', composing: TextRange(start: 5, end: 6)));
|
||||||
expect(state.currentTextEditingValue.text, '12345');
|
expect(state.currentTextEditingValue.text, 'abcde');
|
||||||
expect(state.currentTextEditingValue.composing, TextRange.empty);
|
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<EditableTextState>(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<EditableTextState>(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<EditableTextState>(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<EditableTextState>(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<EditableTextState>(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<EditableTextState>(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', () {
|
group('callback errors', () {
|
||||||
const String errorText = 'Test EditableText callback error';
|
const String errorText = 'Test EditableText callback error';
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('onSaved callback is called', (WidgetTester tester) async {
|
testWidgets('onSaved callback is called', (WidgetTester tester) async {
|
||||||
@ -848,7 +849,7 @@ void main() {
|
|||||||
expect(() => builder(), throwsAssertionError);
|
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 {
|
testWidgets('Validate form should return correct validation if the value is composing', (WidgetTester tester) async {
|
||||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||||
String? fieldValue;
|
String? fieldValue;
|
||||||
@ -864,6 +865,7 @@ void main() {
|
|||||||
key: formKey,
|
key: formKey,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
maxLength: 5,
|
maxLength: 5,
|
||||||
|
maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
|
||||||
onSaved: (String? value) { fieldValue = value; },
|
onSaved: (String? value) { fieldValue = value; },
|
||||||
validator: (String? value) => (value != null && value.length > 5) ? 'Exceeded' : null,
|
validator: (String? value) => (value != null && value.length > 5) ? 'Exceeded' : null,
|
||||||
),
|
),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user