Unverified Commit d81c8aa8 authored by Renzo Olivares's avatar Renzo Olivares Committed by GitHub

SelectionArea long press selection overlay behavior should match native (#133967)

During a long press, on native iOS the context menu does not show until the long press has ended. The handles are shown immediately when the long press begins. This is true for static and editable text.

For static text on Android, the context menu appears when the long press is initiated, but the handles do not appear until the long press has ended. For editable text on Android, the context menu does not appear until the long press ended, and the handles also do not appear until the end.

For both platforms in editable/static contexts the context menu does not show while doing a long press drag.

I think the behavior where the context menu is not shown until the long press ends makes the most sense even though Android varies in this depending on the context. The user is not able to react to the context menu until the long press has ended.

Other details:
On a windows touch screen device the context menu does not show up until the long press ends in editable/static text contexts. On a long press hold it selects the word on drag start as well as popping up the selection handles (static text).
parent c21bf45b
...@@ -539,8 +539,12 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe ...@@ -539,8 +539,12 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
widget.focusNode.requestFocus(); widget.focusNode.requestFocus();
_selectWordAt(offset: details.globalPosition); _selectWordAt(offset: details.globalPosition);
_showToolbar(); // Platforms besides Android will show the text selection handles when
// the long press is initiated. Android shows the text selection handles when
// the long press has ended, usually after a pointer up event is received.
if (defaultTargetPlatform != TargetPlatform.android) {
_showHandles(); _showHandles();
}
_updateSelectedContentIfNeeded(); _updateSelectedContentIfNeeded();
} }
...@@ -552,6 +556,10 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe ...@@ -552,6 +556,10 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
void _handleTouchLongPressEnd(LongPressEndDetails details) { void _handleTouchLongPressEnd(LongPressEndDetails details) {
_finalizeSelection(); _finalizeSelection();
_updateSelectedContentIfNeeded(); _updateSelectedContentIfNeeded();
_showToolbar();
if (defaultTargetPlatform == TargetPlatform.android) {
_showHandles();
}
} }
bool _positionIsOnActiveSelection({required Offset globalPosition}) { bool _positionIsOnActiveSelection({required Offset globalPosition}) {
......
...@@ -151,6 +151,8 @@ void main() { ...@@ -151,6 +151,8 @@ void main() {
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
// `are` is selected. // `are` is selected.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7)); expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
await gesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget);
...@@ -189,6 +191,8 @@ void main() { ...@@ -189,6 +191,8 @@ void main() {
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
// `are` is selected. // `are` is selected.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7)); expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
await gesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing); expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing);
...@@ -262,6 +266,7 @@ void main() { ...@@ -262,6 +266,7 @@ void main() {
await gesture.up(); await gesture.up();
final List<TextBox> boxes = paragraph2.getBoxesForSelection(paragraph2.selections[0]); final List<TextBox> boxes = paragraph2.getBoxesForSelection(paragraph2.selections[0]);
expect(boxes.length, 1); expect(boxes.length, 1);
await tester.pumpAndSettle();
// There is a selection now. // There is a selection now.
// We check the presence of the copy button to make sure the selection toolbar // We check the presence of the copy button to make sure the selection toolbar
// is showing. // is showing.
......
...@@ -561,6 +561,7 @@ void main() { ...@@ -561,6 +561,7 @@ void main() {
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
await gesture.up(); await gesture.up();
await tester.pumpAndSettle();
expect(paragraph0.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4)); expect(paragraph0.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4));
final List<TextBox> boxes = paragraph0.getBoxesForSelection(paragraph0.selections[0]); final List<TextBox> boxes = paragraph0.getBoxesForSelection(paragraph0.selections[0]);
...@@ -619,6 +620,7 @@ void main() { ...@@ -619,6 +620,7 @@ void main() {
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
await gesture.up(); await gesture.up();
await tester.pumpAndSettle();
expect(paragraph0.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4)); expect(paragraph0.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4));
final List<TextBox> boxes = paragraph0.getBoxesForSelection(paragraph0.selections[0]); final List<TextBox> boxes = paragraph0.getBoxesForSelection(paragraph0.selections[0]);
...@@ -674,6 +676,7 @@ void main() { ...@@ -674,6 +676,7 @@ void main() {
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
await gesture.up(); await gesture.up();
await tester.pumpAndSettle();
expect(paragraph0.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4)); expect(paragraph0.selections[0], const TextSelection(baseOffset: 0, extentOffset: 4));
final List<TextBox> boxes = paragraph0.getBoxesForSelection(paragraph0.selections[0]); final List<TextBox> boxes = paragraph0.getBoxesForSelection(paragraph0.selections[0]);
......
...@@ -949,6 +949,100 @@ void main() { ...@@ -949,6 +949,100 @@ void main() {
await gesture.up(); await gesture.up();
}); });
testWidgets(
'long press selection overlay behavior on iOS and Android',
(WidgetTester tester) async {
// This test verifies that all platforms wait until long press end to
// show the context menu, and only Android waits until long press end to
// show the selection handles.
final bool isPlatformAndroid = defaultTargetPlatform == TargetPlatform.android;
Set<ContextMenuButtonType> buttonTypes = <ContextMenuButtonType>{};
final UniqueKey toolbarKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
focusNode: FocusNode(),
selectionControls: materialTextSelectionHandleControls,
contextMenuBuilder: (
BuildContext context,
SelectableRegionState selectableRegionState,
) {
buttonTypes = selectableRegionState.contextMenuButtonItems
.map((ContextMenuButtonItem buttonItem) => buttonItem.type)
.toSet();
return SizedBox.shrink(key: toolbarKey);
},
child: const Text('How are you?'),
),
),
);
expect(buttonTypes.isEmpty, true);
expect(find.byKey(toolbarKey), findsNothing);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2));
addTearDown(gesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await tester.pumpAndSettle();
// All platform except Android should show the selection handles when the
// long press starts.
List<FadeTransition> transitions = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
matching: find.byType(FadeTransition),
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
expect(transitions.length, isPlatformAndroid ? 0 : 2);
FadeTransition? left;
FadeTransition? right;
if (!isPlatformAndroid) {
left = transitions[0];
right = transitions[1];
expect(left.opacity.value, equals(1.0));
expect(right.opacity.value, equals(1.0));
}
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 3));
expect(find.byKey(toolbarKey), findsNothing);
await gesture.moveTo(textOffsetToPosition(paragraph, 8));
await tester.pumpAndSettle();
transitions = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
matching: find.byType(FadeTransition),
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
// All platform except Android should show the selection handles while doing
// a long press drag.
expect(transitions.length, isPlatformAndroid ? 0 : 2);
if (!isPlatformAndroid) {
left = transitions[0];
right = transitions[1];
expect(left.opacity.value, equals(1.0));
expect(right.opacity.value, equals(1.0));
}
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
expect(find.byKey(toolbarKey), findsNothing);
await gesture.up();
await tester.pumpAndSettle();
transitions = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
matching: find.byType(FadeTransition),
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
expect(transitions.length, 2);
left = transitions[0];
right = transitions[1];
// All platforms should show the selection handles and context menu when
// the long press ends.
expect(paragraph.selections[0], const TextSelection(baseOffset: 0, extentOffset: 11));
expect(left.opacity.value, equals(1.0));
expect(right.opacity.value, equals(1.0));
expect(find.byKey(toolbarKey), findsOneWidget);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS }),
skip: kIsWeb, // [intended] Web uses its native context menu.
);
testWidgetsWithLeakTracking( testWidgetsWithLeakTracking(
'single tap on the previous selection toggles the toolbar on iOS', 'single tap on the previous selection toggles the toolbar on iOS',
(WidgetTester tester) async { (WidgetTester tester) async {
...@@ -2695,6 +2789,9 @@ void main() { ...@@ -2695,6 +2789,9 @@ void main() {
// `are` is selected. // `are` is selected.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7)); expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await gesture.up();
await tester.pumpAndSettle();
// Text selection toolbar has appeared. // Text selection toolbar has appeared.
expect(find.text('Copy'), findsOneWidget); expect(find.text('Copy'), findsOneWidget);
...@@ -2862,6 +2959,8 @@ void main() { ...@@ -2862,6 +2959,8 @@ void main() {
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
// `are` is selected. // `are` is selected.
expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7)); expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7));
await gesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(buttonTypes, contains(ContextMenuButtonType.copy)); expect(buttonTypes, contains(ContextMenuButtonType.copy));
......
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