Unverified Commit 135a8c22 authored by Alex Li's avatar Alex Li Committed by GitHub

Introduce `MaxLengthEnforcement` (#68086)

parent 635dfc3e
......@@ -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;
}
// 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);
}
// 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);
}
}
......
......@@ -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);
});
});
}
......@@ -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,
),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment