diff --git a/packages/flutter/lib/src/material/spell_check_suggestions_toolbar.dart b/packages/flutter/lib/src/material/spell_check_suggestions_toolbar.dart index baf8b2ba454..cf0c218c832 100644 --- a/packages/flutter/lib/src/material/spell_check_suggestions_toolbar.dart +++ b/packages/flutter/lib/src/material/spell_check_suggestions_toolbar.dart @@ -10,7 +10,6 @@ import 'adaptive_text_selection_toolbar.dart'; import 'colors.dart'; import 'material.dart'; import 'spell_check_suggestions_toolbar_layout_delegate.dart'; -import 'text_selection_toolbar.dart'; import 'text_selection_toolbar_text_button.dart'; // The default height of the SpellCheckSuggestionsToolbar, which @@ -74,10 +73,6 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget { /// suggestions toolbar. final List buttonItems; - /// Padding between the toolbar and the anchor. Eyeballed on Pixel 4 emulator - /// running Android API 31. - static const double kToolbarContentDistanceBelow = TextSelectionToolbar.kHandleSize - 3.0; - /// Builds the button items for the toolbar based on the available /// spell check suggestions. static List? buildButtonItems( @@ -153,6 +148,8 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget { /// Determines the Offset that the toolbar will be anchored to. static Offset getToolbarAnchor(TextSelectionToolbarAnchors anchors) { + // Since this will be positioned below the anchor point, use the secondary + // anchor by default. return anchors.secondaryAnchor == null ? anchors.primaryAnchor : anchors.secondaryAnchor!; } @@ -190,24 +187,26 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget { final double spellCheckSuggestionsToolbarHeight = _kDefaultToolbarHeight - (48.0 * (4 - buttonItems.length)); // Incorporate the padding distance between the content and toolbar. - final Offset anchorPadded = - anchor + const Offset(0.0, kToolbarContentDistanceBelow); final MediaQueryData mediaQueryData = MediaQuery.of(context); final double softKeyboardViewInsetsBottom = mediaQueryData.viewInsets.bottom; - final double paddingAbove = mediaQueryData.padding.top + CupertinoTextSelectionToolbar.kToolbarScreenPadding; + final double paddingAbove = mediaQueryData.padding.top + + CupertinoTextSelectionToolbar.kToolbarScreenPadding; // Makes up for the Padding. - final Offset localAdjustment = Offset(CupertinoTextSelectionToolbar.kToolbarScreenPadding, paddingAbove); + final Offset localAdjustment = Offset( + CupertinoTextSelectionToolbar.kToolbarScreenPadding, + paddingAbove, + ); return Padding( padding: EdgeInsets.fromLTRB( CupertinoTextSelectionToolbar.kToolbarScreenPadding, - kToolbarContentDistanceBelow, + paddingAbove, CupertinoTextSelectionToolbar.kToolbarScreenPadding, CupertinoTextSelectionToolbar.kToolbarScreenPadding + softKeyboardViewInsetsBottom, ), child: CustomSingleChildLayout( delegate: SpellCheckSuggestionsToolbarLayoutDelegate( - anchor: anchorPadded - localAdjustment, + anchor: anchor - localAdjustment, ), child: AnimatedSize( // This duration was eyeballed on a Pixel 2 emulator running Android diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 3d97ce81c12..573efa736e4 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -3994,7 +3994,8 @@ class EditableTextState extends State with AutomaticKeepAliveClien || platformNotSupported || widget.readOnly || _selectionOverlay == null - || !_spellCheckResultsReceived) { + || !_spellCheckResultsReceived + || findSuggestionSpanAtCursorIndex(textEditingValue.selection.extentOffset) == null) { // Only attempt to show the spell check suggestions toolbar if there // is a toolbar specified and spell check suggestions available to show. return false; diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index a6fce461514..3603132df59 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -2210,12 +2210,12 @@ class TextSelectionGestureDetectorBuilder { // On desktop platforms the selection is set on tap down. case TargetPlatform.android: editableText.hideToolbar(); - editableText.showSpellCheckSuggestionsToolbar(); if (isShiftPressedValid) { _extendSelection(details.globalPosition, SelectionChangedCause.tap); return; } renderEditable.selectPosition(cause: SelectionChangedCause.tap); + editableText.showSpellCheckSuggestionsToolbar(); case TargetPlatform.fuchsia: editableText.hideToolbar(); if (isShiftPressedValid) { @@ -2276,8 +2276,7 @@ class TextSelectionGestureDetectorBuilder { } else { editableText.toggleToolbar(false); } - } - else if (((_positionWasOnSelectionExclusive(textPosition) && !previousSelection.isCollapsed) || (_positionWasOnSelectionInclusive(textPosition) && previousSelection.isCollapsed && isAffinityTheSame)) && renderEditable.hasFocus) { + } else if (((_positionWasOnSelectionExclusive(textPosition) && !previousSelection.isCollapsed) || (_positionWasOnSelectionInclusive(textPosition) && previousSelection.isCollapsed && isAffinityTheSame)) && renderEditable.hasFocus) { editableText.toggleToolbar(false); } else { renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); diff --git a/packages/flutter/test/material/spell_check_suggestions_toolbar_test.dart b/packages/flutter/test/material/spell_check_suggestions_toolbar_test.dart index b4517f9192a..66c668f671b 100644 --- a/packages/flutter/test/material/spell_check_suggestions_toolbar_test.dart +++ b/packages/flutter/test/material/spell_check_suggestions_toolbar_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/cupertino.dart' show CupertinoTextSelectionToolbar; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -50,9 +49,6 @@ void main() { testWidgets('positions toolbar below anchor when it fits above bottom view padding', (WidgetTester tester) async { // We expect the toolbar to be positioned right below the anchor with padding accounted for. - const double expectedToolbarY = - _kAnchor + (2 * SpellCheckSuggestionsToolbar.kToolbarContentDistanceBelow) - CupertinoTextSelectionToolbar.kToolbarScreenPadding; - await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -65,13 +61,12 @@ void main() { ); final double toolbarY = tester.getTopLeft(findSpellCheckSuggestionsToolbar()).dy; - expect(toolbarY, equals(expectedToolbarY)); + expect(toolbarY, equals(_kAnchor)); }); testWidgets('re-positions toolbar higher below anchor when it does not fit above bottom view padding', (WidgetTester tester) async { - // We expect the toolbar to be positioned _kTestToolbarOverlap pixels above the anchor with padding accounted for. - const double expectedToolbarY = - _kAnchor + (2 * SpellCheckSuggestionsToolbar.kToolbarContentDistanceBelow) - CupertinoTextSelectionToolbar.kToolbarScreenPadding - _kTestToolbarOverlap; + // We expect the toolbar to be positioned _kTestToolbarOverlap pixels above the anchor. + const double expectedToolbarY = _kAnchor - _kTestToolbarOverlap; await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 170b5d9db93..ca76cf8ac7e 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -15542,6 +15542,67 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.android }), skip: kIsWeb, // [intended] ); + + testWidgets('tapping on a misspelled word hides the handles', (WidgetTester tester) async { + tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue = + true; + controller.value = const TextEditingValue( + // All misspellings of "test". One the same length, one shorter, and one + // longer. + text: 'test test testt', + selection: TextSelection(affinity: TextAffinity.upstream, baseOffset: 0, extentOffset: 4), + ); + await tester.pumpWidget( + MaterialApp( + home: EditableText( + backgroundCursorColor: Colors.grey, + controller: controller, + focusNode: focusNode, + style: textStyle, + cursorColor: cursorColor, + selectionControls: materialTextSelectionControls, + showSelectionHandles: true, + spellCheckConfiguration: + const SpellCheckConfiguration( + misspelledTextStyle: TextField.materialMisspelledTextStyle, + spellCheckSuggestionsToolbarBuilder: TextField.defaultSpellCheckSuggestionsToolbarBuilder, + ), + ), + ), + ); + + final EditableTextState state = + tester.state(find.byType(EditableText)); + + state.spellCheckResults = SpellCheckResults( + controller.value.text, + const [ + SuggestionSpan(TextRange(start: 10, end: 15), ['test']), + ]); + await tester.tapAt(textOffsetToPosition(tester, 0)); + await tester.pumpAndSettle(); + expect(state.showSpellCheckSuggestionsToolbar(), isFalse); + await tester.pumpAndSettle(); + expect(find.text('test'), findsNothing); + expect(state.selectionOverlay!.handlesAreVisible, isTrue); + + await tester.tapAt(textOffsetToPosition(tester, 12)); + await tester.pumpAndSettle(); + expect(state.showSpellCheckSuggestionsToolbar(), isTrue); + await tester.pumpAndSettle(); + expect(find.text('test'), findsOneWidget); + expect(state.selectionOverlay!.handlesAreVisible, isFalse); + + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pumpAndSettle(); + expect(state.showSpellCheckSuggestionsToolbar(), isFalse); + await tester.pumpAndSettle(); + expect(find.text('test'), findsNothing); + expect(state.selectionOverlay!.handlesAreVisible, isTrue); + }, + variant: const TargetPlatformVariant({ TargetPlatform.android }), + skip: kIsWeb, // [intended] + ); }); group('magnifier', () {