TextField behavior when at maxLength (#52130)

TextFields now disallow adding characters in the center of the text at maxLength.
This commit is contained in:
Justin McCandless 2020-03-18 08:02:22 -07:00 committed by GitHub
parent 78d3e8a71d
commit a811bce4b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 149 additions and 23 deletions

View file

@ -4,6 +4,7 @@
import 'dart:math' as math;
import 'package:flutter/foundation.dart' show visibleForTesting;
import 'text_editing.dart';
import 'text_input.dart';
@ -166,35 +167,48 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter {
/// characters.
final int maxLength;
// TODO(justinmc): This should be updated to use characters instead of runes,
// see the comment in formatEditUpdate.
/// Truncate the given TextEditingValue to maxLength runes.
@visibleForTesting
static TextEditingValue truncate(TextEditingValue value, int maxLength) {
final TextSelection newSelection = value.selection.copyWith(
baseOffset: math.min(value.selection.start, maxLength),
extentOffset: math.min(value.selection.end, maxLength),
);
final RuneIterator iterator = RuneIterator(value.text);
if (iterator.moveNext())
for (int count = 0; count < maxLength; ++count)
if (!iterator.moveNext())
break;
final String truncated = value.text.substring(0, iterator.rawIndex);
return TextEditingValue(
text: truncated,
selection: newSelection,
composing: TextRange.empty,
);
}
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, // unused.
TextEditingValue newValue,
) {
// This does not count grapheme clusters (i.e. characters visible to the user),
// it counts Unicode runes, which leaves out a number of useful possible
// characters (like many emoji), so this will be inaccurate in the
// presence of those characters. The Dart lang bug
// https://github.com/dart-lang/sdk/issues/28404 has been filed to
// address this in Dart.
// TODO(justinmc): convert this to count actual characters using Dart's
// characters package (https://pub.dev/packages/characters).
if (maxLength != null && maxLength > 0 && newValue.text.runes.length > maxLength) {
final TextSelection newSelection = newValue.selection.copyWith(
baseOffset: math.min(newValue.selection.start, maxLength),
extentOffset: math.min(newValue.selection.end, maxLength),
);
// This does not count grapheme clusters (i.e. characters visible to the user),
// it counts Unicode runes, which leaves out a number of useful possible
// characters (like many emoji), so this will be inaccurate in the
// presence of those characters. The Dart lang bug
// https://github.com/dart-lang/sdk/issues/28404 has been filed to
// address this in Dart.
// TODO(gspencer): convert this to count actual characters when Dart
// supports that.
final RuneIterator iterator = RuneIterator(newValue.text);
if (iterator.moveNext())
for (int count = 0; count < maxLength; ++count)
if (!iterator.moveNext())
break;
final String truncated = newValue.text.substring(0, iterator.rawIndex);
return TextEditingValue(
text: truncated,
selection: newSelection,
composing: TextRange.empty,
);
// If already at the maximum and tried to enter even more, keep the old
// value.
if (oldValue.text.runes.length == maxLength) {
return oldValue;
}
return truncate(newValue, maxLength);
}
return newValue;
}

View file

@ -3378,6 +3378,31 @@ void main() {
expect(textController.text, '0123456789');
});
testWidgets('maxLength limits input in the center of a maxed-out field.', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/37420.
final TextEditingController textController = TextEditingController();
const String testValue = '0123456789';
await tester.pumpWidget(boilerplate(
child: TextField(
controller: textController,
maxLength: 10,
),
));
// Max out the character limit in the field.
await tester.enterText(find.byType(TextField), testValue);
expect(textController.text, testValue);
// Entering more characters at the end does nothing.
await tester.enterText(find.byType(TextField), testValue + '9999999');
expect(textController.text, testValue);
// Entering text in the middle of the field also does nothing.
await tester.enterText(find.byType(TextField), '0123455555555556789');
expect(textController.text, testValue);
});
testWidgets('maxLength limits input length even if decoration is null.', (WidgetTester tester) async {
final TextEditingController textController = TextEditingController();

View file

@ -0,0 +1,87 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('LengthLimitingTextInputFormatter', () {
group('truncate', () {
test('Removes characters from the end', () async {
const TextEditingValue value = TextEditingValue(
text: '01234567890',
selection: TextSelection.collapsed(offset: -1),
composing: TextRange.empty,
);
final TextEditingValue truncated = LengthLimitingTextInputFormatter
.truncate(value, 10);
expect(truncated.text, '0123456789');
});
});
group('formatEditUpdate', () {
const int maxLength = 10;
test('Passes through when under limit', () async {
const TextEditingValue oldValue = TextEditingValue(
text: 'aaa',
selection: TextSelection.collapsed(offset: -1),
composing: TextRange.empty,
);
const TextEditingValue newValue = TextEditingValue(
text: 'aaab',
selection: TextSelection.collapsed(offset: -1),
composing: TextRange.empty,
);
final LengthLimitingTextInputFormatter formatter =
LengthLimitingTextInputFormatter(maxLength);
final TextEditingValue formatted = formatter.formatEditUpdate(
oldValue,
newValue
);
expect(formatted.text, newValue.text);
});
test('Uses old value when at the limit', () async {
const TextEditingValue oldValue = TextEditingValue(
text: 'aaaaaaaaaa',
selection: TextSelection.collapsed(offset: -1),
composing: TextRange.empty,
);
const TextEditingValue newValue = TextEditingValue(
text: 'aaaaabbbbbaaaaa',
selection: TextSelection.collapsed(offset: -1),
composing: TextRange.empty,
);
final LengthLimitingTextInputFormatter formatter =
LengthLimitingTextInputFormatter(maxLength);
final TextEditingValue formatted = formatter.formatEditUpdate(
oldValue,
newValue
);
expect(formatted.text, oldValue.text);
});
test('Truncates newValue when oldValue already over limit', () async {
const TextEditingValue oldValue = TextEditingValue(
text: 'aaaaaaaaaaaaaaaaaaaa',
selection: TextSelection.collapsed(offset: -1),
composing: TextRange.empty,
);
const TextEditingValue newValue = TextEditingValue(
text: 'bbbbbbbbbbbbbbbbbbbb',
selection: TextSelection.collapsed(offset: -1),
composing: TextRange.empty,
);
final LengthLimitingTextInputFormatter formatter =
LengthLimitingTextInputFormatter(maxLength);
final TextEditingValue formatted = formatter.formatEditUpdate(
oldValue,
newValue
);
expect(formatted.text, 'bbbbbbbbbb');
});
});
});
}