From 00eeabf584747b93f6d539bb33f01ac970f518ab Mon Sep 17 00:00:00 2001 From: Renzo Olivares Date: Wed, 24 Jul 2024 15:46:22 -0700 Subject: [PATCH] Fix: A selectable's selection under the active selection should not be cleared on right-click (#151851) Fixes #150268 The issue was related to the check for selection geometry here: https://github.com/flutter/flutter/blob/22a5c6cb0a906c219e360ab72da0935aa7a18b93/packages/flutter/lib/src/widgets/selectable_region.dart#L2469-L2476 . Since `otherList == myList` is a reference check this would fail even if the selection rects inside the list contained in SelectionGeometry where the same causing the selectables inside the selection but outside the selectable containing the tapped position to have their selection cleared, use `listEquals` instead. --- .../flutter/lib/src/rendering/selection.dart | 2 +- .../test/widgets/selectable_region_test.dart | 76 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/rendering/selection.dart b/packages/flutter/lib/src/rendering/selection.dart index 18a24db76d..f8d3b60a1f 100644 --- a/packages/flutter/lib/src/rendering/selection.dart +++ b/packages/flutter/lib/src/rendering/selection.dart @@ -713,7 +713,7 @@ class SelectionGeometry { return other is SelectionGeometry && other.startSelectionPoint == startSelectionPoint && other.endSelectionPoint == endSelectionPoint - && other.selectionRects == selectionRects + && listEquals(other.selectionRects, selectionRects) && other.status == status && other.hasContent == hasContent; } diff --git a/packages/flutter/test/widgets/selectable_region_test.dart b/packages/flutter/test/widgets/selectable_region_test.dart index 304b037dd1..430d5e74cc 100644 --- a/packages/flutter/test/widgets/selectable_region_test.dart +++ b/packages/flutter/test/widgets/selectable_region_test.dart @@ -2358,6 +2358,82 @@ void main() { skip: kIsWeb, // [intended] Web uses its native context menu. ); + testWidgets( + 'right-click mouse on an active selection does not clear the selection in other selectables on Apple platforms', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/150268. + Set buttonTypes = {}; + final UniqueKey toolbarKey = UniqueKey(); + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + + 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 Column( + children: [ + Text('How are you?'), + Text('Good, and you?'), + Text('Fine, thank you.'), + ], + ), + ), + ), + ); + + expect(buttonTypes.isEmpty, true); + expect(find.byKey(toolbarKey), findsNothing); + + final RenderParagraph paragraph = tester.renderObject(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText))); + final RenderParagraph paragraph2 = tester.renderObject(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText))); + final RenderParagraph paragraph3 = tester.renderObject(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText))); + final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 2), kind: PointerDeviceKind.mouse); + final TestGesture secondaryMouseButtonGesture = await tester.createGesture(kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton); + addTearDown(secondaryMouseButtonGesture.removePointer); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.moveTo(textOffsetToPosition(paragraph3, 5)); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + expect(paragraph.selections, isNotEmpty); + expect(paragraph2.selections, isNotEmpty); + expect(paragraph3.selections, isNotEmpty); + expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 12)); + expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14)); + expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5)); + + // Right-clicking on the active selection should retain the selection. + await secondaryMouseButtonGesture.down(textOffsetToPosition(paragraph2, 7)); + await tester.pump(); + await secondaryMouseButtonGesture.up(); + await tester.pumpAndSettle(); + expect(paragraph.selections, isNotEmpty); + expect(paragraph2.selections, isNotEmpty); + expect(paragraph3.selections, isNotEmpty); + expect(paragraph.selections[0], const TextSelection(baseOffset: 2, extentOffset: 12)); + expect(paragraph2.selections[0], const TextSelection(baseOffset: 0, extentOffset: 14)); + expect(paragraph3.selections[0], const TextSelection(baseOffset: 0, extentOffset: 5)); + + expect(buttonTypes, contains(ContextMenuButtonType.copy)); + expect(buttonTypes, contains(ContextMenuButtonType.selectAll)); + expect(find.byKey(toolbarKey), findsOneWidget); + }, + variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS }), + skip: kIsWeb, // [intended] Web uses its native context menu. + ); + testWidgets( 'right-click mouse at the same position as previous right-click toggles the context menu on macOS', (WidgetTester tester) async {