From 6fc33134090c137aa3221b8207910eebf8bdb01d Mon Sep 17 00:00:00 2001 From: Renzo Olivares Date: Fri, 17 Oct 2025 12:07:42 -0700 Subject: [PATCH] `SelectableRegion` should use flutter rendered menu on the web for Android and iOS (#177122) This change updates the default context menu when using `SelectionArea`/`SelectableRegion` on Android or iOS web. Previously: the native browser menu would be used. After this change: the flutter rendered context menu is used. Fixes https://github.com/flutter/flutter/issues/151468 - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --------- Co-authored-by: Renzo Olivares --- .../lib/src/widgets/selectable_region.dart | 36 ++- .../test/material/selection_area_test.dart | 44 ++++ .../selectable_region_context_menu_test.dart | 226 ++++++++++-------- .../test/widgets/selectable_region_test.dart | 50 ++++ 4 files changed, 242 insertions(+), 114 deletions(-) diff --git a/packages/flutter/lib/src/widgets/selectable_region.dart b/packages/flutter/lib/src/widgets/selectable_region.dart index 0a33ee8e2dc..9a8b670a7c3 100644 --- a/packages/flutter/lib/src/widgets/selectable_region.dart +++ b/packages/flutter/lib/src/widgets/selectable_region.dart @@ -303,18 +303,21 @@ class SelectableRegion extends StatefulWidget { }) { final bool canCopy = selectionGeometry.status == SelectionStatus.uncollapsed; final bool canSelectAll = selectionGeometry.hasContent; - final bool platformCanShare = switch (defaultTargetPlatform) { - TargetPlatform.android => selectionGeometry.status == SelectionStatus.uncollapsed, - TargetPlatform.macOS || - TargetPlatform.fuchsia || - TargetPlatform.linux || - TargetPlatform.windows => false, - // TODO(bleroux): the share button should be shown on iOS but the share - // functionality requires some changes on the engine side because, on iPad, - // it needs an anchor for the popup. - // See: https://github.com/flutter/flutter/issues/141775. - TargetPlatform.iOS => false, - }; + // The share button is not supported on the web. + final bool platformCanShare = + !kIsWeb && + switch (defaultTargetPlatform) { + TargetPlatform.android => selectionGeometry.status == SelectionStatus.uncollapsed, + TargetPlatform.macOS || + TargetPlatform.fuchsia || + TargetPlatform.linux || + TargetPlatform.windows => false, + // TODO(bleroux): the share button should be shown on iOS but the share + // functionality requires some changes on the engine side because, on iPad, + // it needs an anchor for the popup. + // See: https://github.com/flutter/flutter/issues/141775. + TargetPlatform.iOS => false, + }; final bool canShare = onShare != null && platformCanShare; // On Android, the share button is before the select all button. @@ -410,7 +413,14 @@ class SelectableRegionState extends State SelectedContent? _lastSelectedContent; /// Whether the native browser context menu is enabled. - bool get _webContextMenuEnabled => kIsWeb && BrowserContextMenu.enabled; + // TODO(Renzo-Olivares): Re-enable web context menu for Android + // and iOS when https://github.com/flutter/flutter/issues/177123 + // is resolved. + bool get _webContextMenuEnabled => + kIsWeb && + BrowserContextMenu.enabled && + defaultTargetPlatform != TargetPlatform.android && + defaultTargetPlatform != TargetPlatform.iOS; /// The [SelectionOverlay] that is currently visible on the screen. /// diff --git a/packages/flutter/test/material/selection_area_test.dart b/packages/flutter/test/material/selection_area_test.dart index 5e38d566af8..7e865bbe25b 100644 --- a/packages/flutter/test/material/selection_area_test.dart +++ b/packages/flutter/test/material/selection_area_test.dart @@ -130,6 +130,50 @@ void main() { variant: const TargetPlatformVariant({TargetPlatform.iOS}), ); + testWidgets( + 'builds the default context menu by default on Android and iOS web', + (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + addTearDown(focusNode.dispose); + + await tester.pumpWidget( + MaterialApp( + home: SelectionArea(focusNode: focusNode, child: const Text('How are you?')), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing); + + // Show the toolbar by longpressing. + final RenderParagraph paragraph1 = tester.renderObject( + find.descendant(of: find.text('How are you?'), matching: find.byType(RichText)), + ); + final TestGesture gesture = await tester.startGesture( + textOffsetToPosition(paragraph1, 6), + ); // at the 'r' + addTearDown(gesture.removePointer); + await tester.pump(const Duration(milliseconds: 500)); + // `are` is selected. + expect(paragraph1.selections[0], const TextSelection(baseOffset: 4, extentOffset: 7)); + + await gesture.up(); + await tester.pumpAndSettle(); + + expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); + }, + // TODO(Renzo-Olivares): Remove this test when the web context menu + // for Android and iOS is re-enabled. + // See: https://github.com/flutter/flutter/issues/177123. + // [intended] Android and iOS use the flutter rendered menu on the web. + skip: + !kIsWeb || + !{ + TargetPlatform.android, + TargetPlatform.iOS, + }.contains(defaultTargetPlatform), + ); + testWidgets( 'builds the default context menu by default', (WidgetTester tester) async { diff --git a/packages/flutter/test/widgets/selectable_region_context_menu_test.dart b/packages/flutter/test/widgets/selectable_region_context_menu_test.dart index 6ab42eaa78e..c7c1a70b416 100644 --- a/packages/flutter/test/widgets/selectable_region_context_menu_test.dart +++ b/packages/flutter/test/widgets/selectable_region_context_menu_test.dart @@ -24,6 +24,14 @@ extension on web.CSSRuleList { Iterable.generate(length, (int index) => item(index)); } +bool _browserContextMenuEnabled() { + // TODO(Renzo-Olivares): Remove this when the web context menu + // for Android and iOS is re-enabled. + // See: https://github.com/flutter/flutter/issues/177123. + return kIsWeb && + !{TargetPlatform.android, TargetPlatform.iOS}.contains(defaultTargetPlatform); +} + void main() { late FakePlatformViewRegistry fakePlatformViewRegistry; @@ -39,128 +47,144 @@ void main() { PlatformSelectableRegionContextMenu.debugResetRegistry(); }); - testWidgets('DOM element is set up correctly', (WidgetTester tester) async { - final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + testWidgets( + 'DOM element is set up correctly', + (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - await tester.pumpWidget( - MaterialApp( - home: SelectableRegion( - selectionControls: EmptyTextSelectionControls(), - child: const Placeholder(), + await tester.pumpWidget( + MaterialApp( + home: SelectableRegion( + selectionControls: EmptyTextSelectionControls(), + child: const Placeholder(), + ), ), - ), - ); + ); - final web.HTMLElement element = - fakePlatformViewRegistry.getViewById(currentViewId + 1) as web.HTMLElement; + final web.HTMLElement element = + fakePlatformViewRegistry.getViewById(currentViewId + 1) as web.HTMLElement; - expect(element, isNotNull); - expect(element.style.width, '100%'); - expect(element.style.height, '100%'); - expect(element.classList.length, 1); + expect(element, isNotNull); + expect(element.style.width, '100%'); + expect(element.style.height, '100%'); + expect(element.classList.length, 1); - final int numberOfStyleElements = getNumberOfStyleElements(); - expect(numberOfStyleElements, 1); - }); + final int numberOfStyleElements = getNumberOfStyleElements(); + expect(numberOfStyleElements, 1); + }, + skip: !_browserContextMenuEnabled(), // [intended] + ); - testWidgets('only one