mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
TextField behavior when at maxLength (#52130)
TextFields now disallow adding characters in the center of the text at maxLength.
This commit is contained in:
parent
78d3e8a71d
commit
a811bce4b0
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
87
packages/flutter/test/services/text_formatter_test.dart
Normal file
87
packages/flutter/test/services/text_formatter_test.dart
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue