Unverified Commit 98aaf00a authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

Fix the position of the Android-style spell check toolbar (#124897)

The spell check menu now appears directly below the misspelled word on Android.
parent 3ab8cd26
......@@ -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<ContextMenuButtonItem> 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<ContextMenuButtonItem>? 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
......
......@@ -3994,7 +3994,8 @@ class EditableTextState extends State<EditableText> 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;
......
......@@ -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);
......
......@@ -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(
......
......@@ -15542,6 +15542,67 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
variant: const TargetPlatformVariant(<TargetPlatform>{ 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<EditableTextState>(find.byType(EditableText));
state.spellCheckResults = SpellCheckResults(
controller.value.text,
const <SuggestionSpan>[
SuggestionSpan(TextRange(start: 10, end: 15), <String>['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>{ TargetPlatform.android }),
skip: kIsWeb, // [intended]
);
});
group('magnifier', () {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment