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:
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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: (_) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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: (_) {
|
||||
|
||||
@@ -624,7 +624,6 @@ class _ProviderFormState extends State<_ProviderForm> {
|
||||
keyboardType: TextInputType.url,
|
||||
textInputAction: TextInputAction.done,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
onChanged: _handleChanged,
|
||||
onFieldSubmitted: (_) => _handleSubmit(),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user