mirror of
https://github.com/flutter/flutter
synced 2024-08-27 12:00:58 +00:00
[iOS] Add spell check suggestions toolbar on tap (#119189)
[iOS] Add spell check suggestions toolbar on tap
This commit is contained in:
parent
9d820aa35a
commit
52cacd9d1d
|
@ -58,6 +58,7 @@ export 'src/cupertino/search_field.dart';
|
|||
export 'src/cupertino/segmented_control.dart';
|
||||
export 'src/cupertino/slider.dart';
|
||||
export 'src/cupertino/sliding_segmented_control.dart';
|
||||
export 'src/cupertino/spell_check_suggestions_toolbar.dart';
|
||||
export 'src/cupertino/switch.dart';
|
||||
export 'src/cupertino/tab_scaffold.dart';
|
||||
export 'src/cupertino/tab_view.dart';
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart' show SelectionChangedCause, SuggestionSpan;
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'text_selection_toolbar.dart';
|
||||
import 'text_selection_toolbar_button.dart';
|
||||
|
||||
/// iOS only shows 3 spell check suggestions in the toolbar.
|
||||
const int _maxSuggestions = 3;
|
||||
|
||||
/// The default spell check suggestions toolbar for iOS.
|
||||
///
|
||||
/// Tries to position itself below the [anchors], but if it doesn't fit, then it
|
||||
/// readjusts to fit above bottom view insets.
|
||||
class CupertinoSpellCheckSuggestionsToolbar extends StatelessWidget {
|
||||
/// Constructs a [CupertinoSpellCheckSuggestionsToolbar].
|
||||
const CupertinoSpellCheckSuggestionsToolbar({
|
||||
super.key,
|
||||
required this.anchors,
|
||||
required this.buttonItems,
|
||||
});
|
||||
|
||||
/// The location on which to anchor the menu.
|
||||
final TextSelectionToolbarAnchors anchors;
|
||||
|
||||
/// The [ContextMenuButtonItem]s that will be turned into the correct button
|
||||
/// widgets and displayed in the spell check suggestions toolbar.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [AdaptiveTextSelectionToolbar.buttonItems], the list of
|
||||
/// [ContextMenuButtonItem]s that are used to build the buttons of the
|
||||
/// text selection toolbar.
|
||||
/// * [SpellCheckSuggestionsToolbar.buttonItems], the list of
|
||||
/// [ContextMenuButtonItem]s used to build the Material style spell check
|
||||
/// suggestions toolbar.
|
||||
final List<ContextMenuButtonItem> buttonItems;
|
||||
|
||||
/// Builds the button items for the toolbar based on the available
|
||||
/// spell check suggestions.
|
||||
static List<ContextMenuButtonItem>? buildButtonItems(
|
||||
BuildContext context,
|
||||
EditableTextState editableTextState,
|
||||
) {
|
||||
// Determine if composing region is misspelled.
|
||||
final SuggestionSpan? spanAtCursorIndex =
|
||||
editableTextState.findSuggestionSpanAtCursorIndex(
|
||||
editableTextState.currentTextEditingValue.selection.baseOffset,
|
||||
);
|
||||
|
||||
if (spanAtCursorIndex == null) {
|
||||
return null;
|
||||
}
|
||||
if (spanAtCursorIndex.suggestions.isEmpty) {
|
||||
return <ContextMenuButtonItem>[
|
||||
ContextMenuButtonItem(
|
||||
onPressed: () {},
|
||||
label: 'No Replacements Found',
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
|
||||
|
||||
// Build suggestion buttons.
|
||||
int suggestionCount = 0;
|
||||
for (final String suggestion in spanAtCursorIndex.suggestions) {
|
||||
if (suggestionCount >= _maxSuggestions) {
|
||||
break;
|
||||
}
|
||||
buttonItems.add(ContextMenuButtonItem(
|
||||
onPressed: () {
|
||||
if (!editableTextState.mounted) {
|
||||
return;
|
||||
}
|
||||
_replaceText(
|
||||
editableTextState,
|
||||
suggestion,
|
||||
spanAtCursorIndex.range,
|
||||
);
|
||||
},
|
||||
label: suggestion,
|
||||
));
|
||||
suggestionCount += 1;
|
||||
}
|
||||
return buttonItems;
|
||||
}
|
||||
|
||||
static void _replaceText(EditableTextState editableTextState, String text, TextRange replacementRange) {
|
||||
// Replacement cannot be performed if the text is read only or obscured.
|
||||
assert(!editableTextState.widget.readOnly && !editableTextState.widget.obscureText);
|
||||
|
||||
final TextEditingValue newValue = editableTextState.textEditingValue.replaced(
|
||||
replacementRange,
|
||||
text,
|
||||
);
|
||||
editableTextState.userUpdateTextEditingValue(newValue,SelectionChangedCause.toolbar);
|
||||
|
||||
// Schedule a call to bringIntoView() after renderEditable updates.
|
||||
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
|
||||
if (editableTextState.mounted) {
|
||||
editableTextState.bringIntoView(editableTextState.textEditingValue.selection.extent);
|
||||
}
|
||||
});
|
||||
editableTextState.hideToolbar();
|
||||
editableTextState.renderEditable.selectWordEdge(cause: SelectionChangedCause.toolbar);
|
||||
}
|
||||
|
||||
/// Builds the toolbar buttons based on the [buttonItems].
|
||||
List<Widget> _buildToolbarButtons(BuildContext context) {
|
||||
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
||||
return CupertinoTextSelectionToolbarButton.buttonItem(
|
||||
buttonItem: buttonItem,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Widget> children = _buildToolbarButtons(context);
|
||||
return CupertinoTextSelectionToolbar(
|
||||
anchorAbove: anchors.primaryAnchor,
|
||||
anchorBelow: anchors.secondaryAnchor == null ? anchors.primaryAnchor : anchors.secondaryAnchor!,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import 'colors.dart';
|
|||
import 'desktop_text_selection.dart';
|
||||
import 'icons.dart';
|
||||
import 'magnifier.dart';
|
||||
import 'spell_check_suggestions_toolbar.dart';
|
||||
import 'text_selection.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
|
@ -787,6 +788,32 @@ class CupertinoTextField extends StatefulWidget {
|
|||
decorationStyle: TextDecorationStyle.dotted,
|
||||
);
|
||||
|
||||
/// Default builder for the spell check suggestions toolbar in the Cupertino
|
||||
/// style.
|
||||
///
|
||||
/// See also:
|
||||
/// * [SpellCheckConfiguration.spellCheckSuggestionsToolbarBuilder], the
|
||||
/// builder configured to show a spell check suggestions toolbar.
|
||||
/// * [TextField.defaultSpellCheckSuggestionsToolbarBuilder], the builder
|
||||
/// configured to show the Material style spell check suggestions toolbar.
|
||||
@visibleForTesting
|
||||
static Widget defaultSpellCheckSuggestionsToolbarBuilder(
|
||||
BuildContext context,
|
||||
EditableTextState editableTextState,
|
||||
) {
|
||||
final List<ContextMenuButtonItem>? buttonItems =
|
||||
CupertinoSpellCheckSuggestionsToolbar.buildButtonItems(context, editableTextState);
|
||||
|
||||
if (buttonItems == null || buttonItems.isEmpty){
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return CupertinoSpellCheckSuggestionsToolbar(
|
||||
anchors: editableTextState.contextMenuAnchors,
|
||||
buttonItems: buttonItems,
|
||||
);
|
||||
}
|
||||
|
||||
/// {@macro flutter.widgets.undoHistory.controller}
|
||||
final UndoHistoryController? undoController;
|
||||
|
||||
|
@ -1274,7 +1301,11 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
|
|||
widget.spellCheckConfiguration != const SpellCheckConfiguration.disabled()
|
||||
? widget.spellCheckConfiguration!.copyWith(
|
||||
misspelledTextStyle: widget.spellCheckConfiguration!.misspelledTextStyle
|
||||
?? CupertinoTextField.cupertinoMisspelledTextStyle)
|
||||
?? CupertinoTextField.cupertinoMisspelledTextStyle,
|
||||
spellCheckSuggestionsToolbarBuilder:
|
||||
widget.spellCheckConfiguration!.spellCheckSuggestionsToolbarBuilder
|
||||
?? CupertinoTextField.defaultSpellCheckSuggestionsToolbarBuilder,
|
||||
)
|
||||
: const SpellCheckConfiguration.disabled();
|
||||
|
||||
final Widget paddedEditable = Padding(
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart' show SuggestionSpan;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart' show SelectionChangedCause, SuggestionSpan;
|
||||
|
||||
import 'adaptive_text_selection_toolbar.dart';
|
||||
import 'colors.dart';
|
||||
|
@ -42,6 +43,9 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget {
|
|||
/// * [AdaptiveTextSelectionToolbar.buttonItems], the list of
|
||||
/// [ContextMenuButtonItem]s that are used to build the buttons of the
|
||||
/// text selection toolbar.
|
||||
/// * [CupertinoSpellCheckSuggestionsToolbar.buttonItems], the list of
|
||||
/// [ContextMenuButtonItem]s used to build the Cupertino style spell check
|
||||
/// suggestions toolbar.
|
||||
final List<ContextMenuButtonItem> buttonItems;
|
||||
|
||||
/// Padding between the toolbar and the anchor. Eyeballed on Pixel 4 emulator
|
||||
|
@ -77,10 +81,13 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget {
|
|||
for (final String suggestion in spanAtCursorIndex.suggestions) {
|
||||
buttonItems.add(ContextMenuButtonItem(
|
||||
onPressed: () {
|
||||
editableTextState
|
||||
.replaceComposingRegion(
|
||||
SelectionChangedCause.toolbar,
|
||||
suggestion,
|
||||
if (!editableTextState.mounted) {
|
||||
return;
|
||||
}
|
||||
_replaceText(
|
||||
editableTextState,
|
||||
suggestion,
|
||||
spanAtCursorIndex.range,
|
||||
);
|
||||
},
|
||||
label: suggestion,
|
||||
|
@ -91,9 +98,13 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget {
|
|||
final ContextMenuButtonItem deleteButton =
|
||||
ContextMenuButtonItem(
|
||||
onPressed: () {
|
||||
editableTextState.replaceComposingRegion(
|
||||
SelectionChangedCause.toolbar,
|
||||
if (!editableTextState.mounted) {
|
||||
return;
|
||||
}
|
||||
_replaceText(
|
||||
editableTextState,
|
||||
'',
|
||||
editableTextState.currentTextEditingValue.composing,
|
||||
);
|
||||
},
|
||||
type: ContextMenuButtonType.delete,
|
||||
|
@ -103,6 +114,25 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget {
|
|||
return buttonItems;
|
||||
}
|
||||
|
||||
static void _replaceText(EditableTextState editableTextState, String text, TextRange replacementRange) {
|
||||
// Replacement cannot be performed if the text is read only or obscured.
|
||||
assert(!editableTextState.widget.readOnly && !editableTextState.widget.obscureText);
|
||||
|
||||
final TextEditingValue newValue = editableTextState.textEditingValue.replaced(
|
||||
replacementRange,
|
||||
text,
|
||||
);
|
||||
editableTextState.userUpdateTextEditingValue(newValue, SelectionChangedCause.toolbar);
|
||||
|
||||
// Schedule a call to bringIntoView() after renderEditable updates.
|
||||
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
|
||||
if (editableTextState.mounted) {
|
||||
editableTextState.bringIntoView(editableTextState.textEditingValue.selection.extent);
|
||||
}
|
||||
});
|
||||
editableTextState.hideToolbar();
|
||||
}
|
||||
|
||||
/// Determines the Offset that the toolbar will be anchored to.
|
||||
static Offset getToolbarAnchor(TextSelectionToolbarAnchors anchors) {
|
||||
return anchors.secondaryAnchor == null ? anchors.primaryAnchor : anchors.secondaryAnchor!;
|
||||
|
|
|
@ -805,7 +805,9 @@ class TextField extends StatefulWidget {
|
|||
///
|
||||
/// See also:
|
||||
/// * [SpellCheckConfiguration.spellCheckSuggestionsToolbarBuilder], the
|
||||
// builder configured to show a spell check suggestions toolbar.
|
||||
/// builder configured to show a spell check suggestions toolbar.
|
||||
/// * [CupertinoTextField.defaultSpellCheckSuggestionsToolbarBuilder], the builder
|
||||
/// configured to show the Material style spell check suggestions toolbar.
|
||||
@visibleForTesting
|
||||
static Widget defaultSpellCheckSuggestionsToolbarBuilder(
|
||||
BuildContext context,
|
||||
|
@ -1239,7 +1241,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
|||
?? TextField.materialMisspelledTextStyle,
|
||||
spellCheckSuggestionsToolbarBuilder:
|
||||
widget.spellCheckConfiguration!.spellCheckSuggestionsToolbarBuilder
|
||||
?? TextField.defaultSpellCheckSuggestionsToolbarBuilder
|
||||
?? TextField.defaultSpellCheckSuggestionsToolbarBuilder,
|
||||
)
|
||||
: const SpellCheckConfiguration.disabled();
|
||||
|
||||
|
|
|
@ -15,9 +15,9 @@ import 'system_channels.dart';
|
|||
/// to "Hello, wrold!" may be:
|
||||
/// ```dart
|
||||
/// SuggestionSpan suggestionSpan =
|
||||
/// SuggestionSpan(
|
||||
/// const TextRange(start: 7, end: 12),
|
||||
/// List<String>.of(<String>['word', 'world', 'old']),
|
||||
/// const SuggestionSpan(
|
||||
/// TextRange(start: 7, end: 12),
|
||||
/// <String>['word', 'world', 'old'],
|
||||
/// );
|
||||
/// ```
|
||||
@immutable
|
||||
|
|
|
@ -2347,24 +2347,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||
}
|
||||
}
|
||||
|
||||
/// Replace composing region with specified text.
|
||||
void replaceComposingRegion(SelectionChangedCause cause, String text) {
|
||||
// Replacement cannot be performed if the text is read only or obscured.
|
||||
assert(!widget.readOnly && !widget.obscureText);
|
||||
|
||||
_replaceText(ReplaceTextIntent(textEditingValue, text, textEditingValue.composing, cause));
|
||||
|
||||
if (cause == SelectionChangedCause.toolbar) {
|
||||
// Schedule a call to bringIntoView() after renderEditable updates.
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
bringIntoView(textEditingValue.selection.extent);
|
||||
}
|
||||
});
|
||||
hideToolbar();
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds specified [SuggestionSpan] that matches the provided index using
|
||||
/// binary search.
|
||||
///
|
||||
|
@ -3980,7 +3962,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||
/// Shows toolbar with spell check suggestions of misspelled words that are
|
||||
/// available for click-and-replace.
|
||||
bool showSpellCheckSuggestionsToolbar() {
|
||||
// Spell check suggestions toolbars are intended to be shown on non-web
|
||||
// platforms. Additionally, the Cupertino style toolbar can't be drawn on
|
||||
// the web with the HTML renderer due to
|
||||
// https://github.com/flutter/flutter/issues/123560.
|
||||
final bool platformNotSupported = kIsWeb && BrowserContextMenu.enabled;
|
||||
if (!spellCheckEnabled
|
||||
|| platformNotSupported
|
||||
|| widget.readOnly
|
||||
|| _selectionOverlay == null
|
||||
|| !_spellCheckResultsReceived) {
|
||||
|
|
|
@ -2187,10 +2187,17 @@ class TextSelectionGestureDetectorBuilder {
|
|||
case PointerDeviceKind.trackpad:
|
||||
case PointerDeviceKind.stylus:
|
||||
case PointerDeviceKind.invertedStylus:
|
||||
// Precise devices should place the cursor at a precise position.
|
||||
// TODO(camsim99): Determine spell check toolbar behavior in these cases:
|
||||
// https://github.com/flutter/flutter/issues/119573.
|
||||
// Precise devices should place the cursor at a precise position if the
|
||||
// word at the text position is not misspelled.
|
||||
renderEditable.selectPosition(cause: SelectionChangedCause.tap);
|
||||
case PointerDeviceKind.touch:
|
||||
case PointerDeviceKind.unknown:
|
||||
// If the word that was tapped is misspelled, select the word and show the spell check suggestions
|
||||
// toolbar once. If additional taps are made on a misspelled word, toggle the toolbar. If the word
|
||||
// is not misspelled, default to the following behavior:
|
||||
//
|
||||
// Toggle the toolbar if the `previousSelection` is collapsed, the tap is on the selection, the
|
||||
// TextAffinity remains the same, and the editable is focused. The TextAffinity is important when the
|
||||
// cursor is on the boundary of a line wrap, if the affinity is different (i.e. it is downstream), the
|
||||
|
@ -2205,9 +2212,17 @@ class TextSelectionGestureDetectorBuilder {
|
|||
final TextSelection previousSelection = renderEditable.selection ?? editableText.textEditingValue.selection;
|
||||
final TextPosition textPosition = renderEditable.getPositionForPoint(details.globalPosition);
|
||||
final bool isAffinityTheSame = textPosition.affinity == previousSelection.affinity;
|
||||
if (((_positionWasOnSelectionExclusive(textPosition) && !previousSelection.isCollapsed)
|
||||
|| (_positionWasOnSelectionInclusive(textPosition) && previousSelection.isCollapsed && isAffinityTheSame))
|
||||
&& renderEditable.hasFocus) {
|
||||
final bool wordAtCursorIndexIsMisspelled = editableText.findSuggestionSpanAtCursorIndex(textPosition.offset) != null;
|
||||
|
||||
if (wordAtCursorIndexIsMisspelled) {
|
||||
renderEditable.selectWord(cause: SelectionChangedCause.tap);
|
||||
if (previousSelection != editableText.textEditingValue.selection) {
|
||||
editableText.showSpellCheckSuggestionsToolbar();
|
||||
} else {
|
||||
editableText.toggleToolbar(false);
|
||||
}
|
||||
}
|
||||
else if (((_positionWasOnSelectionExclusive(textPosition) && !previousSelection.isCollapsed) || (_positionWasOnSelectionInclusive(textPosition) && previousSelection.isCollapsed && isAffinityTheSame)) && renderEditable.hasFocus) {
|
||||
editableText.toggleToolbar(false);
|
||||
} else {
|
||||
renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
|
||||
|
|
|
@ -15045,23 +15045,23 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
|
|||
selection: TextSelection(affinity: TextAffinity.upstream, baseOffset: 0, extentOffset: 4),
|
||||
);
|
||||
controller.value = value;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: EditableText(
|
||||
backgroundCursorColor: Colors.grey,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
style: textStyle,
|
||||
cursorColor: cursorColor,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
spellCheckConfiguration:
|
||||
const SpellCheckConfiguration(
|
||||
misspelledTextStyle: TextField.materialMisspelledTextStyle,
|
||||
spellCheckSuggestionsToolbarBuilder: TextField.defaultSpellCheckSuggestionsToolbarBuilder,
|
||||
),
|
||||
),
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: EditableText(
|
||||
backgroundCursorColor: Colors.grey,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
style: textStyle,
|
||||
cursorColor: cursorColor,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
spellCheckConfiguration:
|
||||
const SpellCheckConfiguration(
|
||||
misspelledTextStyle: TextField.materialMisspelledTextStyle,
|
||||
spellCheckSuggestionsToolbarBuilder: TextField.defaultSpellCheckSuggestionsToolbarBuilder,
|
||||
),
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
|
||||
final EditableTextState state =
|
||||
tester.state<EditableTextState>(find.byType(EditableText));
|
||||
|
@ -15085,16 +15085,81 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
|
|||
from: Offset.zero,
|
||||
cause: SelectionChangedCause.tap,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.showSpellCheckSuggestionsToolbar(), true);
|
||||
// Toolbar will only show on non-web platforms.
|
||||
expect(state.showSpellCheckSuggestionsToolbar(), !kIsWeb);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('test'), findsOneWidget);
|
||||
expect(find.text('sets'), findsOneWidget);
|
||||
expect(find.text('set'), findsOneWidget);
|
||||
expect(find.text('DELETE'), findsOneWidget);
|
||||
|
||||
const Matcher matcher = kIsWeb ? findsNothing : findsOneWidget;
|
||||
expect(find.text('test'), matcher);
|
||||
expect(find.text('sets'), matcher);
|
||||
expect(find.text('set'), matcher);
|
||||
expect(find.text('DELETE'), matcher);
|
||||
});
|
||||
|
||||
testWidgets('spell check suggestions toolbar buttons correctly change the composing region', (WidgetTester tester) async {
|
||||
testWidgets('cupertino spell check suggestions toolbar buttons correctly change the composing region', (WidgetTester tester) async {
|
||||
tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue =
|
||||
true;
|
||||
const TextEditingValue value = TextEditingValue(
|
||||
text: 'tset test test',
|
||||
selection: TextSelection(affinity: TextAffinity.upstream, baseOffset: 0, extentOffset: 4),
|
||||
);
|
||||
controller.value = value;
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: EditableText(
|
||||
backgroundCursorColor: Colors.grey,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
style: textStyle,
|
||||
cursorColor: cursorColor,
|
||||
selectionControls: cupertinoTextSelectionControls,
|
||||
spellCheckConfiguration:
|
||||
const SpellCheckConfiguration(
|
||||
misspelledTextStyle: CupertinoTextField.cupertinoMisspelledTextStyle,
|
||||
spellCheckSuggestionsToolbarBuilder: CupertinoTextField.defaultSpellCheckSuggestionsToolbarBuilder,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final EditableTextState state =
|
||||
tester.state<EditableTextState>(find.byType(EditableText));
|
||||
state.spellCheckResults = const SpellCheckResults('tset test test', <SuggestionSpan>[SuggestionSpan(TextRange(start: 0, end: 4), <String>['test', 'sets', 'set'])]);
|
||||
state.renderEditable.selectWordsInRange(
|
||||
from: Offset.zero,
|
||||
cause: SelectionChangedCause.tap,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Set last tap down position so that selecting the word edge will be
|
||||
// a valid operation.
|
||||
final Offset pos1 = textOffsetToPosition(tester, 1);
|
||||
final TestGesture gesture = await tester.startGesture(pos1);
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.currentTextEditingValue.selection.baseOffset, equals(1));
|
||||
|
||||
// Test that tapping misspelled word replacement buttons will replace
|
||||
// the correct word and select the word edge.
|
||||
state.showSpellCheckSuggestionsToolbar();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
if (kIsWeb) {
|
||||
expect(find.text('sets'), findsNothing);
|
||||
}
|
||||
else {
|
||||
expect(find.text('sets'), findsOneWidget);
|
||||
await tester.tap(find.text('sets'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.currentTextEditingValue.text, equals('sets test test'));
|
||||
expect(state.currentTextEditingValue.selection.baseOffset, equals(4));
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('material spell check suggestions toolbar buttons correctly change the composing region', (WidgetTester tester) async {
|
||||
tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue =
|
||||
true;
|
||||
const TextEditingValue value = TextEditingValue(
|
||||
|
@ -15129,21 +15194,34 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
|
|||
cause: SelectionChangedCause.tap,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.currentTextEditingValue.selection.baseOffset, equals(0));
|
||||
|
||||
// Test misspelled word replacement buttons.
|
||||
state.showSpellCheckSuggestionsToolbar();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('sets'), findsOneWidget);
|
||||
await tester.tap(find.text('sets'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.currentTextEditingValue.text, equals('sets test test'));
|
||||
|
||||
if (kIsWeb) {
|
||||
expect(find.text('sets'), findsNothing);
|
||||
} else {
|
||||
expect(find.text('sets'), findsOneWidget);
|
||||
await tester.tap(find.text('sets'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.currentTextEditingValue.text, equals('sets test test'));
|
||||
expect(state.currentTextEditingValue.selection.baseOffset, equals(0));
|
||||
}
|
||||
|
||||
// Test delete button.
|
||||
state.showSpellCheckSuggestionsToolbar();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('DELETE'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.currentTextEditingValue.text, equals(' test test'));
|
||||
if (kIsWeb) {
|
||||
expect(find.text('DELETE'), findsNothing);
|
||||
} else {
|
||||
expect(find.text('DELETE'), findsOneWidget);
|
||||
await tester.tap(find.text('DELETE'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.currentTextEditingValue.text, equals(' test test'));
|
||||
expect(state.currentTextEditingValue.selection.baseOffset, equals(0));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'clipboard_utils.dart';
|
||||
import 'editable_text_utils.dart';
|
||||
|
||||
const int kSingleTapUpTimeout = 500;
|
||||
|
||||
void main() {
|
||||
late int tapCount;
|
||||
late int singleTapUpCount;
|
||||
|
@ -688,6 +690,47 @@ void main() {
|
|||
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }));
|
||||
|
||||
testWidgets('test TextSelectionGestureDetectorBuilder shows spell check toolbar on single tap on iOS if word misspelled and text selection toolbar on additonal taps', (WidgetTester tester) async {
|
||||
await pumpTextSelectionGestureDetectorBuilder(tester);
|
||||
final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
|
||||
final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
|
||||
const TextSelection selection = TextSelection.collapsed(offset: 1);
|
||||
state.updateEditingValue(const TextEditingValue(text: 'something misspelled', selection: selection));
|
||||
|
||||
// Mark word to be tapped as misspelled for testing.
|
||||
state.markCurrentSelectionAsMisspelled = true;
|
||||
await tester.pump();
|
||||
|
||||
// Test spell check suggestions toolbar is shown on first tap of misspelled word.
|
||||
const Offset position = Offset(25.0, 200.0);
|
||||
await tester.tapAt(position);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(state.showSpellCheckSuggestionsToolbarCalled, isTrue);
|
||||
|
||||
// Reset and test text selection toolbar is toggled for additional taps.
|
||||
state.showSpellCheckSuggestionsToolbarCalled = false;
|
||||
renderEditable.selection = selection;
|
||||
await tester.pump(const Duration(milliseconds: kSingleTapUpTimeout));
|
||||
|
||||
// Test first tap.
|
||||
await tester.tapAt(position);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(state.showSpellCheckSuggestionsToolbarCalled, isFalse);
|
||||
expect(state.toggleToolbarCalled, isTrue);
|
||||
|
||||
// Reset and test second tap.
|
||||
state.toggleToolbarCalled = false;
|
||||
await tester.pump(const Duration(milliseconds: kSingleTapUpTimeout));
|
||||
|
||||
await tester.tapAt(position);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(state.showSpellCheckSuggestionsToolbarCalled, isFalse);
|
||||
expect(state.toggleToolbarCalled, isTrue);
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
|
||||
|
||||
testWidgets('test TextSelectionGestureDetectorBuilder double tap', (WidgetTester tester) async {
|
||||
await pumpTextSelectionGestureDetectorBuilder(tester);
|
||||
final TestGesture gesture = await tester.startGesture(
|
||||
|
@ -1646,6 +1689,7 @@ class FakeEditableTextState extends EditableTextState {
|
|||
bool showToolbarCalled = false;
|
||||
bool toggleToolbarCalled = false;
|
||||
bool showSpellCheckSuggestionsToolbarCalled = false;
|
||||
bool markCurrentSelectionAsMisspelled = false;
|
||||
|
||||
@override
|
||||
RenderEditable get renderEditable => _editableKey.currentContext!.findRenderObject()! as RenderEditable;
|
||||
|
@ -1668,6 +1712,15 @@ class FakeEditableTextState extends EditableTextState {
|
|||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
SuggestionSpan? findSuggestionSpanAtCursorIndex(int cursorIndex) {
|
||||
return markCurrentSelectionAsMisspelled
|
||||
? const SuggestionSpan(
|
||||
TextRange(start: 7, end: 12),
|
||||
<String>['word', 'world', 'old'],
|
||||
) : null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
|
|
Loading…
Reference in a new issue