fix(ui): stop non-secret fields from opening a password keyboard

Changelog: Fix pasting and Chinese input in text fields
This commit is contained in:
chen08209
2026-10-17 02:26:15 +08:00
parent 766085995b
commit 5607f7047f
11 changed files with 47 additions and 11 deletions
+10
View File
@@ -75,6 +75,16 @@ clear of the button on either side.
`test/lint/typography_test.dart` fails on `textTheme` anywhere in `lib/` but `typography.dart`, and on `fontSize:` or `test/lint/typography_test.dart` fails on `textTheme` anywhere in `lib/` but `typography.dart`, and on `fontSize:` or
`fontWeight:` outside `lib/ui/`, except in the code editor, whose text follows the editor font-size setting. `fontWeight:` outside `lib/ui/`, except in the code editor, whose text follows the editor font-size setting.
### Text Field Suggestions
On Android, Flutter maps `enableSuggestions: false` to the visible-password input type, not only to "no suggestions"
(`TextInputPlugin.inputTypeFromTextInputType` in the engine). Input methods then treat the field as a password: no
clipboard panel, no Chinese layouts, and on some systems a secure keyboard. With the app's clipboard permission denied,
that leaves no way to paste a link at all. Only the secret fields, `SecretTextFormField` and `FormRowSecretTextField`,
ask for a password keyboard; a link, key, rule or regex field that should not be corrected sets `autocorrect: false`
alone. `test/lint/text_field_suggestions_test.dart` fails on `enableSuggestions: false` or
`TextInputType.visiblePassword` anywhere else in `lib/`.
### Length Units ### Length Units
Flutter lengths are logical pixels, the unit Android calls dp. Comments, test names, docs and reports give them in dp, Flutter lengths are logical pixels, the unit Android calls dp. Comments, test names, docs and reports give them in dp,
-1
View File
@@ -36,7 +36,6 @@ class BatchSection<T> extends StatelessWidget {
maxLines: null, maxLines: null,
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
autocorrect: false, autocorrect: false,
enableSuggestions: false,
onChanged: onChanged, onChanged: onChanged,
decoration: InputDecoration.collapsed( decoration: InputDecoration.collapsed(
border: const NoInputBorder(), border: const NoInputBorder(),
-2
View File
@@ -288,7 +288,6 @@ class _MapEntryFormState extends State<_MapEntryForm> {
hintText: appLocalizations.required, hintText: appLocalizations.required,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
autocorrect: false, autocorrect: false,
enableSuggestions: false,
inputFormatters: keyMaxLength == null inputFormatters: keyMaxLength == null
? null ? null
: TextInputLimits.limit(keyMaxLength), : TextInputLimits.limit(keyMaxLength),
@@ -305,7 +304,6 @@ class _MapEntryFormState extends State<_MapEntryForm> {
hintText: appLocalizations.required, hintText: appLocalizations.required,
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
autocorrect: false, autocorrect: false,
enableSuggestions: false,
inputFormatters: valueMaxLength == null inputFormatters: valueMaxLength == null
? null ? null
: TextInputLimits.limit(valueMaxLength), : TextInputLimits.limit(valueMaxLength),
-1
View File
@@ -296,7 +296,6 @@ class _NamedUrlFormState extends State<NamedUrlForm> {
? TextInputAction.next ? TextInputAction.next
: TextInputAction.done, : TextInputAction.done,
autocorrect: false, autocorrect: false,
enableSuggestions: false,
inputFormatters: TextInputLimits.limit(TextInputLimits.url), inputFormatters: TextInputLimits.limit(TextInputLimits.url),
onChanged: _handleChanged, onChanged: _handleChanged,
onFieldSubmitted: (_) { onFieldSubmitted: (_) {
-1
View File
@@ -374,7 +374,6 @@ class _RuleFormState extends State<RuleForm> {
controller: _contentController, controller: _contentController,
hintText: appLocalizations.inputRuleContent, hintText: appLocalizations.inputRuleContent,
autocorrect: false, autocorrect: false,
enableSuggestions: false,
inputFormatters: TextInputLimits.limit(TextInputLimits.rule), inputFormatters: TextInputLimits.limit(TextInputLimits.rule),
onChanged: (_) { onChanged: (_) {
if (_attempted) { if (_attempted) {
-2
View File
@@ -441,7 +441,6 @@ class _WebDAVFormState extends ConsumerState<WebDAVForm> {
keyboardType: TextInputType.url, keyboardType: TextInputType.url,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
autocorrect: false, autocorrect: false,
enableSuggestions: false,
inputFormatters: TextInputLimits.limit(TextInputLimits.uri), inputFormatters: TextInputLimits.limit(TextInputLimits.uri),
onChanged: _handleChanged, onChanged: _handleChanged,
), ),
@@ -454,7 +453,6 @@ class _WebDAVFormState extends ConsumerState<WebDAVForm> {
hintText: appLocalizations.required, hintText: appLocalizations.required,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
autocorrect: false, autocorrect: false,
enableSuggestions: false,
inputFormatters: TextInputLimits.limit( inputFormatters: TextInputLimits.limit(
TextInputLimits.userName, TextInputLimits.userName,
), ),
-1
View File
@@ -269,7 +269,6 @@ class _FilterFormState extends State<_FilterForm> {
controller: _regexController, controller: _regexController,
hintText: r'(?i)hk|hong\s*kong', hintText: r'(?i)hk|hong\s*kong',
autocorrect: false, autocorrect: false,
enableSuggestions: false,
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
inputFormatters: TextInputLimits.limit(TextInputLimits.filter), inputFormatters: TextInputLimits.limit(TextInputLimits.filter),
onChanged: (_) { onChanged: (_) {
-1
View File
@@ -624,7 +624,6 @@ class _ProviderFormState extends State<_ProviderForm> {
keyboardType: TextInputType.url, keyboardType: TextInputType.url,
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
autocorrect: false, autocorrect: false,
enableSuggestions: false,
onChanged: _handleChanged, onChanged: _handleChanged,
onFieldSubmitted: (_) => _handleSubmit(), onFieldSubmitted: (_) => _handleSubmit(),
), ),
-1
View File
@@ -454,7 +454,6 @@ class _ProfileUrlField extends StatelessWidget {
keyboardType: TextInputType.url, keyboardType: TextInputType.url,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
autocorrect: false, autocorrect: false,
enableSuggestions: false,
inputFormatters: TextInputLimits.limit(TextInputLimits.url), inputFormatters: TextInputLimits.limit(TextInputLimits.url),
onChanged: onChanged, onChanged: onChanged,
), ),
-1
View File
@@ -703,7 +703,6 @@ class _HexColorFieldState extends State<HexColorField> {
child: TextField( child: TextField(
controller: _controller, controller: _controller,
autocorrect: false, autocorrect: false,
enableSuggestions: false,
textCapitalization: TextCapitalization.characters, textCapitalization: TextCapitalization.characters,
style: const TextStyle(fontFeatures: [FontFeature.tabularFigures()]), style: const TextStyle(fontFeatures: [FontFeature.tabularFigures()]),
decoration: InputDecoration( decoration: InputDecoration(
@@ -0,0 +1,37 @@
import 'dart:io';
import 'package:test/test.dart';
final _passwordInputType = RegExp(
r'\benableSuggestions:\s*false\b|\bTextInputType\.visiblePassword\b',
);
const _secretFields = {'lib/ui/input.dart', 'lib/features/form/form_row.dart'};
void main() {
test('only secret fields ask Android for a password keyboard', () {
final offenders = <String>[];
for (final entity in Directory('lib').listSync(recursive: true)) {
if (entity is! File ||
!entity.path.endsWith('.dart') ||
entity.path.contains('/generated/') ||
_secretFields.contains(entity.path)) {
continue;
}
final lines = entity.readAsLinesSync();
for (var i = 0; i < lines.length; i++) {
final match = _passwordInputType.firstMatch(lines[i]);
if (match != null) {
offenders.add(
'${entity.path}:${i + 1} — ${match[0]} makes Android input '
'methods treat the field as a password, with no clipboard or '
'Chinese input; keep autocorrect: false alone.',
);
}
}
}
expect(offenders, isEmpty, reason: offenders.join('\n'));
});
}