Introduce MaxLengthEnforcement
(#68086)
This commit is contained in:
parent
635dfc3e4f
commit
135a8c22b1
@ -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<String>? onChanged;
|
||||
|
||||
@ -761,6 +781,7 @@ class CupertinoTextField extends StatefulWidget {
|
||||
properties.add(DiagnosticsProperty<bool>('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', maxLengthEnforcement, defaultValue: null));
|
||||
properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
|
||||
properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
|
||||
@ -783,6 +804,9 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> 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<CupertinoTextField> with Restoratio
|
||||
final Offset cursorOffset = Offset(_iOSHorizontalCursorOffsetPixels / MediaQuery.of(context).devicePixelRatio, 0);
|
||||
final List<TextInputFormatter> formatters = <TextInputFormatter>[
|
||||
...?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);
|
||||
|
||||
|
@ -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<bool>('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', maxLengthEnforcement, defaultValue: null));
|
||||
properties.add(EnumProperty<TextInputAction>('textInputAction', textInputAction, defaultValue: null));
|
||||
properties.add(EnumProperty<TextCapitalization>('textCapitalization', textCapitalization, defaultValue: TextCapitalization.none));
|
||||
properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: TextAlign.start));
|
||||
@ -835,6 +852,9 @@ class _TextFieldState extends State<TextField> 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<TextField> with RestorationMixin implements
|
||||
final FocusNode focusNode = _effectiveFocusNode;
|
||||
final List<TextInputFormatter> formatters = <TextInputFormatter>[
|
||||
...?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<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(
|
||||
cursor: effectiveMouseCursor,
|
||||
onEnter: (PointerEnterEvent event) => _handleHover(true),
|
||||
@ -1236,9 +1270,7 @@ class _TextFieldState extends State<TextField> 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)
|
||||
|
@ -163,6 +163,7 @@ class TextFormField extends FormField<String> {
|
||||
)
|
||||
bool autovalidate = false,
|
||||
bool maxLengthEnforced = true,
|
||||
MaxLengthEnforcement? maxLengthEnforcement,
|
||||
int? maxLines = 1,
|
||||
int? minLines,
|
||||
bool expands = false,
|
||||
@ -202,6 +203,10 @@ class TextFormField extends FormField<String> {
|
||||
'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<String> {
|
||||
smartQuotesType: smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
||||
enableSuggestions: enableSuggestions,
|
||||
maxLengthEnforced: maxLengthEnforced,
|
||||
maxLengthEnforcement: maxLengthEnforcement,
|
||||
maxLines: maxLines,
|
||||
minLines: minLines,
|
||||
expands: expands,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4302,4 +4302,126 @@ void main() {
|
||||
|
||||
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(
|
||||
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<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,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: <TextInputFormatter>[formatter],
|
||||
style: textStyle,
|
||||
cursorColor: cursorColor,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
),
|
||||
);
|
||||
group('Length formatter', () {
|
||||
const int maxLength = 5;
|
||||
|
||||
await tester.pumpWidget(widget);
|
||||
Future<void> setupWidget(
|
||||
WidgetTester tester,
|
||||
LengthLimitingTextInputFormatter formatter,
|
||||
) async {
|
||||
final Widget widget = MaterialApp(
|
||||
home: EditableText(
|
||||
backgroundCursorColor: Colors.grey,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
inputFormatters: <TextInputFormatter>[formatter],
|
||||
style: textStyle,
|
||||
cursorColor: cursorColor,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
),
|
||||
);
|
||||
|
||||
final EditableTextState state = tester.state<EditableTextState>(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<EditableTextState>(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: <TextInputFormatter>[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<EditableTextState>(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<EditableTextState>(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<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', () {
|
||||
|
@ -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<FormState> formKey = GlobalKey<FormState>();
|
||||
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,
|
||||
),
|
||||
|
Loading…
x
Reference in New Issue
Block a user