From 21018df0292ba0840e11f8a6d60882bb3a33b598 Mon Sep 17 00:00:00 2001 From: Koji Wakamiya Date: Sat, 12 Apr 2025 04:44:06 +0900 Subject: [PATCH] [iOS] Infer autocorrect value from autofillHints (#165637) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix https://github.com/flutter/flutter/issues/134723 If autofillHints contains password-related hints, set autocorrect to `false`. https://github.com/user-attachments/assets/aeb95f74-cabc-40ff-8829-c49bce466a6d I checked the behaviour in UIKit and it is necessary to set correction and spell check to `No` if username or password is set to "Content Type". The following video shows the combination of `TextField` and option in UIKit. https://github.com/user-attachments/assets/6c442a87-042a-4b34-b88f-7a438fefdbb3 code: https://github.com/koji-1009/pm_behavior_test/tree/native ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com> --- .../lib/src/cupertino/search_field.dart | 4 +- .../flutter/lib/src/cupertino/text_field.dart | 6 +- packages/flutter/lib/src/material/search.dart | 4 +- .../flutter/lib/src/material/text_field.dart | 7 +- .../lib/src/widgets/editable_text.dart | 39 ++++++++- .../test/widgets/editable_text_test.dart | 81 +++++++++++++++++++ 6 files changed, 130 insertions(+), 11 deletions(-) diff --git a/packages/flutter/lib/src/cupertino/search_field.dart b/packages/flutter/lib/src/cupertino/search_field.dart index a6e4fbefeb0..aba27885876 100644 --- a/packages/flutter/lib/src/cupertino/search_field.dart +++ b/packages/flutter/lib/src/cupertino/search_field.dart @@ -274,7 +274,9 @@ class CupertinoSearchTextField extends StatefulWidget { /// {@macro flutter.material.textfield.onTap} final VoidCallback? onTap; - /// {@macro flutter.widgets.editableText.autocorrect} + /// Whether to enable autocorrection. + /// + /// Defaults to true. final bool autocorrect; /// Whether to allow the platform to automatically format quotes. diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart index a682133bb06..8932bd1ec77 100644 --- a/packages/flutter/lib/src/cupertino/text_field.dart +++ b/packages/flutter/lib/src/cupertino/text_field.dart @@ -400,7 +400,7 @@ class CupertinoTextField extends StatefulWidget { this.autofocus = false, this.obscuringCharacter = '•', this.obscureText = false, - this.autocorrect = true, + this.autocorrect, SmartDashesType? smartDashesType, SmartQuotesType? smartQuotesType, this.enableSuggestions = true, @@ -619,7 +619,7 @@ class CupertinoTextField extends StatefulWidget { final bool obscureText; /// {@macro flutter.widgets.editableText.autocorrect} - final bool autocorrect; + final bool? autocorrect; /// {@macro flutter.services.TextInputConfiguration.smartDashesType} final SmartDashesType smartDashesType; @@ -920,7 +920,7 @@ class CupertinoTextField extends StatefulWidget { DiagnosticsProperty('obscuringCharacter', obscuringCharacter, defaultValue: '•'), ); properties.add(DiagnosticsProperty('obscureText', obscureText, defaultValue: false)); - properties.add(DiagnosticsProperty('autocorrect', autocorrect, defaultValue: true)); + properties.add(DiagnosticsProperty('autocorrect', autocorrect, defaultValue: null)); properties.add( EnumProperty( 'smartDashesType', diff --git a/packages/flutter/lib/src/material/search.dart b/packages/flutter/lib/src/material/search.dart index 065dda5d68d..dd3dbdb288b 100644 --- a/packages/flutter/lib/src/material/search.dart +++ b/packages/flutter/lib/src/material/search.dart @@ -370,7 +370,9 @@ abstract class SearchDelegate { /// Defaults to the default value specified in [TextField]. final TextInputType? keyboardType; - /// {@macro flutter.widgets.editableText.autocorrect} + /// Whether to enable autocorrection. + /// + /// Defaults to true. final bool autocorrect; /// {@macro flutter.services.TextInputConfiguration.enableSuggestions} diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 3301ef0f10d..02977f41630 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -264,7 +264,7 @@ class TextField extends StatefulWidget { this.statesController, this.obscuringCharacter = '•', this.obscureText = false, - this.autocorrect = true, + this.autocorrect, SmartDashesType? smartDashesType, SmartQuotesType? smartQuotesType, this.enableSuggestions = true, @@ -485,7 +485,7 @@ class TextField extends StatefulWidget { final bool obscureText; /// {@macro flutter.widgets.editableText.autocorrect} - final bool autocorrect; + final bool? autocorrect; /// {@macro flutter.services.TextInputConfiguration.smartDashesType} final SmartDashesType smartDashesType; @@ -961,7 +961,7 @@ class TextField extends StatefulWidget { DiagnosticsProperty('obscuringCharacter', obscuringCharacter, defaultValue: '•'), ); properties.add(DiagnosticsProperty('obscureText', obscureText, defaultValue: false)); - properties.add(DiagnosticsProperty('autocorrect', autocorrect, defaultValue: true)); + properties.add(DiagnosticsProperty('autocorrect', autocorrect, defaultValue: null)); properties.add( EnumProperty( 'smartDashesType', @@ -1695,6 +1695,7 @@ class _TextFieldState extends State dragStartBehavior: widget.dragStartBehavior, scrollController: widget.scrollController, scrollPhysics: widget.scrollPhysics, + autofillHints: widget.autofillHints, autofillClient: this, autocorrectionTextRectColor: autocorrectionTextRectColor, clipBehavior: widget.clipBehavior, diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 3dbcaaf2c5e..0fcd23d2fe7 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -826,7 +826,7 @@ class EditableText extends StatefulWidget { this.readOnly = false, this.obscuringCharacter = '•', this.obscureText = false, - this.autocorrect = true, + bool? autocorrect, SmartDashesType? smartDashesType, SmartQuotesType? smartQuotesType, this.enableSuggestions = true, @@ -908,6 +908,7 @@ class EditableText extends StatefulWidget { this.magnifierConfiguration = TextMagnifierConfiguration.disabled, this.undoController, }) : assert(obscuringCharacter.length == 1), + autocorrect = autocorrect ?? _inferAutocorrect(autofillHints: autofillHints), smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), smartQuotesType = @@ -1050,7 +1051,7 @@ class EditableText extends StatefulWidget { /// {@template flutter.widgets.editableText.autocorrect} /// Whether to enable autocorrection. /// - /// Defaults to true. + /// False on iOS if [autofillHints] contains password-related hints, otherwise true. /// {@endtemplate} final bool autocorrect; @@ -2108,6 +2109,38 @@ class EditableText extends StatefulWidget { return resultButtonItem; } + // Infer the value of autocorrect from autofillHints. + static bool _inferAutocorrect({required Iterable? autofillHints}) { + if (autofillHints == null || autofillHints.isEmpty || kIsWeb) { + return true; + } + + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + // username, password and newPassword are password related hint. + // newUsername is not supported on iOS. + final bool passwordRelatedHint = autofillHints.any( + (String hint) => + hint == AutofillHints.username || + hint == AutofillHints.password || + hint == AutofillHints.newPassword, + ); + if (passwordRelatedHint) { + // https://github.com/flutter/flutter/issues/134723 + // Set autocorrect to false to prevent password bar from flashing. + return false; + } + case TargetPlatform.macOS: + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + break; + } + + return true; + } + // Infer the keyboard type of an `EditableText` if it's not specified. static TextInputType _inferKeyboardType({ required Iterable? autofillHints, @@ -2260,7 +2293,7 @@ class EditableText extends StatefulWidget { properties.add(DiagnosticsProperty('focusNode', focusNode)); properties.add(DiagnosticsProperty('obscureText', obscureText, defaultValue: false)); properties.add(DiagnosticsProperty('readOnly', readOnly, defaultValue: false)); - properties.add(DiagnosticsProperty('autocorrect', autocorrect, defaultValue: true)); + properties.add(DiagnosticsProperty('autocorrect', autocorrect, defaultValue: null)); properties.add( EnumProperty( 'smartDashesType', diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 512b33c3592..05bbcf37071 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -3803,6 +3803,87 @@ void main() { }, ); + testWidgets( + 'iOS autocorrect value is inferred from AutofillHints - username', + (WidgetTester tester) async { + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: TextDirection.ltr, + child: EditableText( + controller: controller, + backgroundCursorColor: Colors.grey, + focusNode: focusNode, + style: textStyle, + cursorColor: cursorColor, + autofillHints: const [AutofillHints.username], + ), + ), + ), + ); + + final EditableText editableText = tester.firstWidget(find.byType(EditableText)); + expect(editableText.autocorrect, isFalse); + }, + variant: const TargetPlatformVariant({TargetPlatform.iOS}), + skip: kIsWeb, // [intended] + ); + + testWidgets( + 'iOS autocorrect value is inferred from AutofillHints - password', + (WidgetTester tester) async { + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: TextDirection.ltr, + child: EditableText( + controller: controller, + backgroundCursorColor: Colors.grey, + focusNode: focusNode, + style: textStyle, + cursorColor: cursorColor, + autofillHints: const [AutofillHints.password], + ), + ), + ), + ); + + final EditableText editableText = tester.firstWidget(find.byType(EditableText)); + expect(editableText.autocorrect, isFalse); + }, + variant: const TargetPlatformVariant({TargetPlatform.iOS}), + skip: kIsWeb, // [intended] + ); + + testWidgets( + 'iOS autocorrect value is inferred from AutofillHints - newPassword', + (WidgetTester tester) async { + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: TextDirection.ltr, + child: EditableText( + controller: controller, + backgroundCursorColor: Colors.grey, + focusNode: focusNode, + style: textStyle, + cursorColor: cursorColor, + autofillHints: const [AutofillHints.newPassword], + ), + ), + ), + ); + + final EditableText editableText = tester.firstWidget(find.byType(EditableText)); + expect(editableText.autocorrect, isFalse); + }, + variant: const TargetPlatformVariant({TargetPlatform.iOS}), + skip: kIsWeb, // [intended] + ); + testWidgets('Changing controller updates EditableText', (WidgetTester tester) async { final TextEditingController controller1 = TextEditingController(text: 'Wibble'); addTearDown(controller1.dispose);