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 4d68938d22
commit 6c52d7a39a
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
`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
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,
keyboardType: TextInputType.multiline,
autocorrect: false,
enableSuggestions: false,
onChanged: onChanged,
decoration: InputDecoration.collapsed(
border: const NoInputBorder(),
-2
View File
@@ -288,7 +288,6 @@ class _MapEntryFormState extends State<_MapEntryForm> {
hintText: appLocalizations.required,
textInputAction: TextInputAction.next,
autocorrect: false,
enableSuggestions: false,
inputFormatters: keyMaxLength == null
? null
: TextInputLimits.limit(keyMaxLength),
@@ -305,7 +304,6 @@ class _MapEntryFormState extends State<_MapEntryForm> {
hintText: appLocalizations.required,
textInputAction: TextInputAction.done,
autocorrect: false,
enableSuggestions: false,
inputFormatters: valueMaxLength == null
? null
: TextInputLimits.limit(valueMaxLength),
-1
View File
@@ -296,7 +296,6 @@ class _NamedUrlFormState extends State<NamedUrlForm> {
? TextInputAction.next
: TextInputAction.done,
autocorrect: false,
enableSuggestions: false,
inputFormatters: TextInputLimits.limit(TextInputLimits.url),
onChanged: _handleChanged,
onFieldSubmitted: (_) {
-1
View File
@@ -374,7 +374,6 @@ class _RuleFormState extends State<RuleForm> {
controller: _contentController,
hintText: appLocalizations.inputRuleContent,
autocorrect: false,
enableSuggestions: false,
inputFormatters: TextInputLimits.limit(TextInputLimits.rule),
onChanged: (_) {
if (_attempted) {
-2
View File
@@ -441,7 +441,6 @@ class _WebDAVFormState extends ConsumerState<WebDAVForm> {
keyboardType: TextInputType.url,
textInputAction: TextInputAction.next,
autocorrect: false,
enableSuggestions: false,
inputFormatters: TextInputLimits.limit(TextInputLimits.uri),
onChanged: _handleChanged,
),
@@ -454,7 +453,6 @@ class _WebDAVFormState extends ConsumerState<WebDAVForm> {
hintText: appLocalizations.required,
textInputAction: TextInputAction.next,
autocorrect: false,
enableSuggestions: false,
inputFormatters: TextInputLimits.limit(
TextInputLimits.userName,
),
-1
View File
@@ -269,7 +269,6 @@ class _FilterFormState extends State<_FilterForm> {
controller: _regexController,
hintText: r'(?i)hk|hong\s*kong',
autocorrect: false,
enableSuggestions: false,
textInputAction: TextInputAction.done,
inputFormatters: TextInputLimits.limit(TextInputLimits.filter),
onChanged: (_) {
-1
View File
@@ -624,7 +624,6 @@ class _ProviderFormState extends State<_ProviderForm> {
keyboardType: TextInputType.url,
textInputAction: TextInputAction.done,
autocorrect: false,
enableSuggestions: false,
onChanged: _handleChanged,
onFieldSubmitted: (_) => _handleSubmit(),
),
-1
View File
@@ -450,7 +450,6 @@ class _ProfileUrlField extends StatelessWidget {
keyboardType: TextInputType.url,
textInputAction: TextInputAction.next,
autocorrect: false,
enableSuggestions: false,
inputFormatters: TextInputLimits.limit(TextInputLimits.url),
onChanged: onChanged,
),
-1
View File
@@ -703,7 +703,6 @@ class _HexColorFieldState extends State<HexColorField> {
child: TextField(
controller: _controller,
autocorrect: false,
enableSuggestions: false,
textCapitalization: TextCapitalization.characters,
style: const TextStyle(fontFeatures: [FontFeature.tabularFigures()]),
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'));
});
}