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
|
`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,
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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: (_) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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: (_) {
|
||||||
|
|||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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'));
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user