Skip to content

Commit 2c477b1

Browse files
SelectableRegion onSelectionChange should be called when the selection changes (#134215)
This change makes sure to call `onSelectionChange` in all cases when selection might change including: * Dragging selection handles * Mouse drag to select * Keyboard actions * Long press drag to select
1 parent c7665ff commit 2c477b1

File tree

2 files changed

+256
-9
lines changed

2 files changed

+256
-9
lines changed

packages/flutter/lib/src/widgets/selectable_region.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,13 +502,15 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
502502
case 2:
503503
_selectWordAt(offset: details.globalPosition);
504504
}
505+
_updateSelectedContentIfNeeded();
505506
}
506507

507508
void _handleMouseDragStart(TapDragStartDetails details) {
508509
switch (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount)) {
509510
case 1:
510511
_selectStartTo(offset: details.globalPosition);
511512
}
513+
_updateSelectedContentIfNeeded();
512514
}
513515

514516
void _handleMouseDragUpdate(TapDragUpdateDetails details) {
@@ -518,6 +520,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
518520
case 2:
519521
_selectEndTo(offset: details.globalPosition, continuous: true, textGranularity: TextGranularity.word);
520522
}
523+
_updateSelectedContentIfNeeded();
521524
}
522525

523526
void _handleMouseDragEnd(TapDragEndDetails details) {
@@ -543,6 +546,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
543546

544547
void _handleTouchLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
545548
_selectEndTo(offset: details.globalPosition, textGranularity: TextGranularity.word);
549+
_updateSelectedContentIfNeeded();
546550
}
547551

548552
void _handleTouchLongPressEnd(LongPressEndDetails details) {
@@ -717,6 +721,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
717721
details.globalPosition,
718722
_selectionDelegate.value.startSelectionPoint!,
719723
));
724+
_updateSelectedContentIfNeeded();
720725
}
721726

722727
void _handleSelectionStartHandleDragUpdate(DragUpdateDetails details) {
@@ -730,6 +735,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
730735
details.globalPosition,
731736
_selectionDelegate.value.startSelectionPoint!,
732737
));
738+
_updateSelectedContentIfNeeded();
733739
}
734740

735741
void _handleSelectionEndHandleDragStart(DragStartDetails details) {
@@ -742,6 +748,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
742748
details.globalPosition,
743749
_selectionDelegate.value.endSelectionPoint!,
744750
));
751+
_updateSelectedContentIfNeeded();
745752
}
746753

747754
void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) {
@@ -755,6 +762,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
755762
details.globalPosition,
756763
_selectionDelegate.value.endSelectionPoint!,
757764
));
765+
_updateSelectedContentIfNeeded();
758766
}
759767

760768
MagnifierInfo _buildInfoForMagnifier(Offset globalGesturePosition, SelectionPoint selectionPoint) {
@@ -1067,6 +1075,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
10671075
granularity: granularity,
10681076
),
10691077
);
1078+
_updateSelectedContentIfNeeded();
10701079
}
10711080

10721081
double? _directionalHorizontalBaseline;
@@ -1088,6 +1097,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
10881097
dx: globalSelectionPointOffset.dx,
10891098
),
10901099
);
1100+
_updateSelectedContentIfNeeded();
10911101
}
10921102

10931103
// [TextSelectionDelegate] overrides.

packages/flutter/test/widgets/selectable_region_test.dart

Lines changed: 246 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2713,7 +2713,7 @@ void main() {
27132713
skip: kIsWeb, // [intended]
27142714
);
27152715

2716-
testWidgets('onSelectionChange is called when the selection changes', (WidgetTester tester) async {
2716+
testWidgets('onSelectionChange is called when the selection changes through gestures', (WidgetTester tester) async {
27172717
SelectedContent? content;
27182718

27192719
await tester.pumpWidget(
@@ -2728,27 +2728,264 @@ void main() {
27282728
),
27292729
),
27302730
);
2731+
27312732
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you'), matching: find.byType(RichText)));
2732-
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph, 4), kind: PointerDeviceKind.mouse);
2733+
final TestGesture mouseGesture = await tester.startGesture(textOffsetToPosition(paragraph, 4), kind: PointerDeviceKind.mouse);
2734+
final TestGesture touchGesture = await tester.createGesture();
2735+
27332736
expect(content, isNull);
2734-
addTearDown(gesture.removePointer);
2737+
addTearDown(mouseGesture.removePointer);
2738+
addTearDown(touchGesture.removePointer);
27352739
await tester.pump();
27362740

2737-
await gesture.moveTo(textOffsetToPosition(paragraph, 7));
2738-
await gesture.up();
2739-
await tester.pump();
2741+
// Called on drag.
2742+
await mouseGesture.moveTo(textOffsetToPosition(paragraph, 7));
2743+
await tester.pumpAndSettle();
27402744
expect(content, isNotNull);
27412745
expect(content!.plainText, 'are');
27422746

2747+
// Updates on drag.
2748+
await mouseGesture.moveTo(textOffsetToPosition(paragraph, 10));
2749+
await tester.pumpAndSettle();
2750+
expect(content, isNotNull);
2751+
expect(content!.plainText, 'are yo');
2752+
2753+
// Called on drag end.
2754+
await mouseGesture.up();
2755+
await tester.pump();
2756+
expect(content, isNotNull);
2757+
expect(content!.plainText, 'are yo');
2758+
27432759
// Backwards selection.
2744-
await gesture.down(textOffsetToPosition(paragraph, 3));
2760+
await mouseGesture.down(textOffsetToPosition(paragraph, 3));
27452761
await tester.pumpAndSettle();
27462762
expect(content, isNull);
2747-
await gesture.moveTo(textOffsetToPosition(paragraph, 0));
2748-
await gesture.up();
2763+
2764+
await mouseGesture.moveTo(textOffsetToPosition(paragraph, 0));
2765+
await tester.pumpAndSettle();
2766+
expect(content, isNotNull);
2767+
expect(content!.plainText, 'How');
2768+
2769+
await mouseGesture.up();
2770+
await tester.pump();
2771+
expect(content, isNotNull);
2772+
expect(content!.plainText, 'How');
2773+
2774+
// Called on double tap.
2775+
await mouseGesture.down(textOffsetToPosition(paragraph, 6));
2776+
await tester.pump();
2777+
await mouseGesture.up();
27492778
await tester.pump();
2779+
await mouseGesture.down(textOffsetToPosition(paragraph, 6));
2780+
await tester.pumpAndSettle();
2781+
expect(content, isNotNull);
2782+
expect(content!.plainText, 'are');
2783+
await mouseGesture.up();
2784+
await tester.pumpAndSettle();
2785+
2786+
// Called on tap.
2787+
await mouseGesture.down(textOffsetToPosition(paragraph, 0));
2788+
await tester.pumpAndSettle();
2789+
expect(content, isNull);
2790+
await mouseGesture.up();
2791+
await tester.pumpAndSettle();
2792+
2793+
// With touch gestures.
2794+
2795+
// Called on long press start.
2796+
await touchGesture.down(textOffsetToPosition(paragraph, 0));
2797+
await tester.pumpAndSettle(kLongPressTimeout);
27502798
expect(content, isNotNull);
27512799
expect(content!.plainText, 'How');
2800+
2801+
// Called on long press update.
2802+
await touchGesture.moveTo(textOffsetToPosition(paragraph, 5));
2803+
await tester.pumpAndSettle();
2804+
expect(content, isNotNull);
2805+
expect(content!.plainText, 'How are');
2806+
2807+
// Called on long press end.
2808+
await touchGesture.up();
2809+
await tester.pumpAndSettle();
2810+
expect(content, isNotNull);
2811+
expect(content!.plainText, 'How are');
2812+
2813+
// Long press to select 'you'.
2814+
await touchGesture.down(textOffsetToPosition(paragraph, 9));
2815+
await tester.pumpAndSettle(kLongPressTimeout);
2816+
expect(content, isNotNull);
2817+
expect(content!.plainText, 'you');
2818+
await touchGesture.up();
2819+
await tester.pumpAndSettle();
2820+
2821+
// Called while moving selection handles.
2822+
final List<TextBox> boxes = paragraph.getBoxesForSelection(paragraph.selections[0]);
2823+
expect(boxes.length, 1);
2824+
final Offset startHandlePos = globalize(boxes[0].toRect().bottomLeft, paragraph);
2825+
final Offset endHandlePos = globalize(boxes[0].toRect().bottomRight, paragraph);
2826+
final Offset startPos = Offset(textOffsetToPosition(paragraph, 4).dx, startHandlePos.dy);
2827+
final Offset endPos = Offset(textOffsetToPosition(paragraph, 6).dx, endHandlePos.dy);
2828+
2829+
// Start handle.
2830+
await touchGesture.down(startHandlePos);
2831+
await touchGesture.moveTo(startPos);
2832+
await tester.pumpAndSettle();
2833+
expect(content, isNotNull);
2834+
expect(content!.plainText, 'are you');
2835+
await touchGesture.up();
2836+
await tester.pumpAndSettle();
2837+
2838+
// End handle.
2839+
await touchGesture.down(endHandlePos);
2840+
await touchGesture.moveTo(endPos);
2841+
await tester.pumpAndSettle();
2842+
expect(content, isNotNull);
2843+
expect(content!.plainText, 'ar');
2844+
await touchGesture.up();
2845+
await tester.pumpAndSettle();
2846+
});
2847+
2848+
testWidgets('onSelectionChange is called when the selection changes through keyboard actions', (WidgetTester tester) async {
2849+
SelectedContent? content;
2850+
2851+
await tester.pumpWidget(
2852+
MaterialApp(
2853+
home: SelectableRegion(
2854+
onSelectionChanged: (SelectedContent? selectedContent) => content = selectedContent,
2855+
focusNode: FocusNode(),
2856+
selectionControls: materialTextSelectionControls,
2857+
child: const Column(
2858+
children: <Widget>[
2859+
Text('How are you?'),
2860+
Text('Good, and you?'),
2861+
Text('Fine, thank you.'),
2862+
],
2863+
),
2864+
),
2865+
),
2866+
);
2867+
2868+
expect(content, isNull);
2869+
await tester.pump();
2870+
2871+
final RenderParagraph paragraph1 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)));
2872+
final RenderParagraph paragraph2 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Good, and you?'), matching: find.byType(RichText)));
2873+
final RenderParagraph paragraph3 = tester.renderObject<RenderParagraph>(find.descendant(of: find.text('Fine, thank you.'), matching: find.byType(RichText)));
2874+
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(paragraph1, 2), kind: PointerDeviceKind.mouse);
2875+
addTearDown(gesture.removePointer);
2876+
await tester.pump();
2877+
await gesture.moveTo(textOffsetToPosition(paragraph1, 6));
2878+
await gesture.up();
2879+
await tester.pump();
2880+
2881+
expect(paragraph1.selections.length, 1);
2882+
expect(paragraph1.selections[0].start, 2);
2883+
expect(paragraph1.selections[0].end, 6);
2884+
expect(content, isNotNull);
2885+
expect(content!.plainText, 'w ar');
2886+
2887+
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true));
2888+
await tester.pump();
2889+
expect(paragraph1.selections.length, 1);
2890+
expect(paragraph1.selections[0].start, 2);
2891+
expect(paragraph1.selections[0].end, 7);
2892+
expect(content, isNotNull);
2893+
expect(content!.plainText, 'w are');
2894+
2895+
for (int i = 0; i < 5; i += 1) {
2896+
await sendKeyCombination(tester,
2897+
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true));
2898+
await tester.pump();
2899+
expect(paragraph1.selections.length, 1);
2900+
expect(paragraph1.selections[0].start, 2);
2901+
expect(paragraph1.selections[0].end, 8 + i);
2902+
expect(content, isNotNull);
2903+
}
2904+
expect(content, isNotNull);
2905+
expect(content!.plainText, 'w are you?');
2906+
2907+
for (int i = 0; i < 5; i += 1) {
2908+
await sendKeyCombination(tester,
2909+
const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true));
2910+
await tester.pump();
2911+
expect(paragraph1.selections.length, 1);
2912+
expect(paragraph1.selections[0].start, 2);
2913+
expect(paragraph1.selections[0].end, 11 - i);
2914+
expect(content, isNotNull);
2915+
}
2916+
expect(content, isNotNull);
2917+
expect(content!.plainText, 'w are');
2918+
2919+
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true));
2920+
await tester.pump();
2921+
expect(paragraph1.selections.length, 1);
2922+
expect(paragraph1.selections[0].start, 2);
2923+
expect(paragraph1.selections[0].end, 12);
2924+
expect(paragraph2.selections.length, 1);
2925+
expect(paragraph2.selections[0].start, 0);
2926+
expect(paragraph2.selections[0].end, 8);
2927+
expect(content, isNotNull);
2928+
expect(content!.plainText, 'w are you?Good, an');
2929+
2930+
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true));
2931+
await tester.pump();
2932+
expect(paragraph1.selections.length, 1);
2933+
expect(paragraph1.selections[0].start, 2);
2934+
expect(paragraph1.selections[0].end, 12);
2935+
expect(paragraph2.selections.length, 1);
2936+
expect(paragraph2.selections[0].start, 0);
2937+
expect(paragraph2.selections[0].end, 14);
2938+
expect(paragraph3.selections.length, 1);
2939+
expect(paragraph3.selections[0].start, 0);
2940+
expect(paragraph3.selections[0].end, 9);
2941+
expect(content, isNotNull);
2942+
expect(content!.plainText, 'w are you?Good, and you?Fine, tha');
2943+
2944+
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true));
2945+
await tester.pump();
2946+
expect(paragraph1.selections.length, 1);
2947+
expect(paragraph1.selections[0].start, 2);
2948+
expect(paragraph1.selections[0].end, 12);
2949+
expect(paragraph2.selections.length, 1);
2950+
expect(paragraph2.selections[0].start, 0);
2951+
expect(paragraph2.selections[0].end, 14);
2952+
expect(paragraph3.selections.length, 1);
2953+
expect(paragraph3.selections[0].start, 0);
2954+
expect(paragraph3.selections[0].end, 16);
2955+
expect(content, isNotNull);
2956+
expect(content!.plainText, 'w are you?Good, and you?Fine, thank you.');
2957+
2958+
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
2959+
await tester.pump();
2960+
expect(paragraph1.selections.length, 1);
2961+
expect(paragraph1.selections[0].start, 2);
2962+
expect(paragraph1.selections[0].end, 12);
2963+
expect(paragraph2.selections.length, 1);
2964+
expect(paragraph2.selections[0].start, 0);
2965+
expect(paragraph2.selections[0].end, 8);
2966+
expect(paragraph3.selections.length, 0);
2967+
expect(content, isNotNull);
2968+
expect(content!.plainText, 'w are you?Good, an');
2969+
2970+
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
2971+
await tester.pump();
2972+
expect(paragraph1.selections.length, 1);
2973+
expect(paragraph1.selections[0].start, 2);
2974+
expect(paragraph1.selections[0].end, 7);
2975+
expect(paragraph2.selections.length, 0);
2976+
expect(paragraph3.selections.length, 0);
2977+
expect(content, isNotNull);
2978+
expect(content!.plainText, 'w are');
2979+
2980+
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true));
2981+
await tester.pump();
2982+
expect(paragraph1.selections.length, 1);
2983+
expect(paragraph1.selections[0].start, 0);
2984+
expect(paragraph1.selections[0].end, 2);
2985+
expect(paragraph2.selections.length, 0);
2986+
expect(paragraph3.selections.length, 0);
2987+
expect(content, isNotNull);
2988+
expect(content!.plainText, 'Ho');
27522989
});
27532990

27542991
group('BrowserContextMenu', () {

0 commit comments

Comments
 (0)