From f16f6d7f9946ed7892de33c2f7b00a631b7230af Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Thu, 17 Jul 2025 11:40:50 -0700 Subject: [PATCH] No SystemContextMenu when readOnly is true (#171242) When readOnly is true, there is no TextInputConnection, so the SystemContextMenu can't be shown. Instead, this shows the Flutter-drawn context menu. This video shows the change. You can slightly tell that the system context menu is shown when readOnly is false and the Flutter-drawn context menu is shown when it is true. https://github.com/user-attachments/assets/91480fa4-cce6-4d63-ae11-df72a229da73 This is the same root cause as https://github.com/flutter/flutter/pull/169238. Fixes https://github.com/flutter/flutter/issues/170521 --- .../flutter/lib/src/cupertino/text_field.dart | 2 +- .../src/cupertino/text_form_field_row.dart | 3 +- .../lib/src/material/search_anchor.dart | 7 +- .../flutter/lib/src/material/text_field.dart | 2 +- .../lib/src/material/text_form_field.dart | 3 +- .../lib/src/widgets/system_context_menu.dart | 23 ++- .../test/cupertino/text_field_test.dart | 82 +++++++++ .../cupertino/text_form_field_row_test.dart | 83 +++++++++ .../test/material/search_anchor_test.dart | 84 ++++++++++ .../test/material/text_field_test.dart | 84 ++++++++++ .../test/material/text_form_field_test.dart | 84 ++++++++++ .../widgets/system_context_menu_test.dart | 158 ++++++++++++++++++ 12 files changed, 607 insertions(+), 8 deletions(-) diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart index 285d3e1db99..3c6096e619f 100644 --- a/packages/flutter/lib/src/cupertino/text_field.dart +++ b/packages/flutter/lib/src/cupertino/text_field.dart @@ -815,7 +815,7 @@ class CupertinoTextField extends StatefulWidget { BuildContext context, EditableTextState editableTextState, ) { - if (defaultTargetPlatform == TargetPlatform.iOS && SystemContextMenu.isSupported(context)) { + if (SystemContextMenu.isSupportedByField(editableTextState)) { return SystemContextMenu.editableText(editableTextState: editableTextState); } return CupertinoAdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); diff --git a/packages/flutter/lib/src/cupertino/text_form_field_row.dart b/packages/flutter/lib/src/cupertino/text_form_field_row.dart index 828f378518a..f9febb900e4 100644 --- a/packages/flutter/lib/src/cupertino/text_form_field_row.dart +++ b/packages/flutter/lib/src/cupertino/text_form_field_row.dart @@ -4,7 +4,6 @@ import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; -import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -280,7 +279,7 @@ class CupertinoTextFormFieldRow extends FormField { BuildContext context, EditableTextState editableTextState, ) { - if (defaultTargetPlatform == TargetPlatform.iOS && SystemContextMenu.isSupported(context)) { + if (SystemContextMenu.isSupportedByField(editableTextState)) { return SystemContextMenu.editableText(editableTextState: editableTextState); } return CupertinoAdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); diff --git a/packages/flutter/lib/src/material/search_anchor.dart b/packages/flutter/lib/src/material/search_anchor.dart index 955a24206da..302be04f63c 100644 --- a/packages/flutter/lib/src/material/search_anchor.dart +++ b/packages/flutter/lib/src/material/search_anchor.dart @@ -1373,6 +1373,7 @@ class SearchBar extends StatefulWidget { this.keyboardType, this.scrollPadding = const EdgeInsets.all(20.0), this.contextMenuBuilder = _defaultContextMenuBuilder, + this.readOnly = false, }); /// Controls the text being edited in the search bar's text field. @@ -1532,11 +1533,14 @@ class SearchBar extends StatefulWidget { /// be disabled and Flutter-rendered context menus to appear. final EditableTextContextMenuBuilder? contextMenuBuilder; + /// {@macro flutter.widgets.editableText.readOnly} + final bool readOnly; + static Widget _defaultContextMenuBuilder( BuildContext context, EditableTextState editableTextState, ) { - if (defaultTargetPlatform == TargetPlatform.iOS && SystemContextMenu.isSupported(context)) { + if (SystemContextMenu.isSupportedByField(editableTextState)) { return SystemContextMenu.editableText(editableTextState: editableTextState); } return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); @@ -1700,6 +1704,7 @@ class _SearchBarState extends State { child: Semantics( inputType: SemanticsInputType.search, child: TextField( + readOnly: widget.readOnly, autofocus: widget.autoFocus, onTap: widget.onTap, onTapAlwaysCalled: true, diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index dd1f81f5b5d..beccf53a355 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -879,7 +879,7 @@ class TextField extends StatefulWidget { BuildContext context, EditableTextState editableTextState, ) { - if (defaultTargetPlatform == TargetPlatform.iOS && SystemContextMenu.isSupported(context)) { + if (SystemContextMenu.isSupportedByField(editableTextState)) { return SystemContextMenu.editableText(editableTextState: editableTextState); } return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); diff --git a/packages/flutter/lib/src/material/text_form_field.dart b/packages/flutter/lib/src/material/text_form_field.dart index 8f61b694ec2..3815b2aff51 100644 --- a/packages/flutter/lib/src/material/text_form_field.dart +++ b/packages/flutter/lib/src/material/text_form_field.dart @@ -4,7 +4,6 @@ import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; -import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -331,7 +330,7 @@ class TextFormField extends FormField { BuildContext context, EditableTextState editableTextState, ) { - if (defaultTargetPlatform == TargetPlatform.iOS && SystemContextMenu.isSupported(context)) { + if (SystemContextMenu.isSupportedByField(editableTextState)) { return SystemContextMenu.editableText(editableTextState: editableTextState); } return AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState); diff --git a/packages/flutter/lib/src/widgets/system_context_menu.dart b/packages/flutter/lib/src/widgets/system_context_menu.dart index c7c25cf2a9b..946b9a5dce6 100644 --- a/packages/flutter/lib/src/widgets/system_context_menu.dart +++ b/packages/flutter/lib/src/widgets/system_context_menu.dart @@ -114,8 +114,29 @@ class SystemContextMenu extends StatefulWidget { /// Whether the current device supports showing the system context menu. /// /// Currently, this is only supported on newer versions of iOS. + /// + /// See also: + /// + /// * [isSupportedByField], which uses this method and determines whether an + /// individual [EditableTextState] supports the system context menu. static bool isSupported(BuildContext context) { - return MediaQuery.maybeSupportsShowingSystemContextMenu(context) ?? false; + return defaultTargetPlatform == TargetPlatform.iOS && + (MediaQuery.maybeSupportsShowingSystemContextMenu(context) ?? false); + } + + /// Whether the given field supports showing the system context menu. + /// + /// Currently [SystemContextMenu] is only supported with an active + /// [TextInputConnection]. In cases where this isn't possible, such as in a + /// read-only field, fall back to using a Flutter-rendered context menu like + /// [AdaptiveTextSelectionToolbar]. + /// + /// See also: + /// + /// * [isSupported], which is used by this method and determines whether the + /// platform in general supports showing the system context menu. + static bool isSupportedByField(EditableTextState editableTextState) { + return !editableTextState.widget.readOnly && isSupported(editableTextState.context); } /// The default [items] for the given [EditableTextState]. diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index 2cdf8abeee6..a620205c811 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -10674,4 +10674,86 @@ void main() { }, variant: TargetPlatformVariant.all(), ); + + testWidgets( + 'readOnly disallows SystemContextMenu', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/170521. + tester.platformDispatcher.supportsShowingSystemContextMenu = true; + final TextEditingController controller = TextEditingController(text: 'abcdefghijklmnopqr'); + addTearDown(() { + tester.platformDispatcher.resetSupportsShowingSystemContextMenu(); + tester.view.reset(); + controller.dispose(); + }); + + bool readOnly = true; + late StateSetter setState; + + await tester.pumpWidget( + // Don't wrap with the global View so that the change to + // platformDispatcher is read. + wrapWithView: false, + View( + view: tester.view, + child: CupertinoApp( + home: StatefulBuilder( + builder: (BuildContext context, StateSetter setter) { + setState = setter; + return CupertinoTextField(readOnly: readOnly, controller: controller); + }, + ), + ), + ), + ); + + final Duration waitDuration = SelectionOverlay.fadeDuration > kDoubleTapTimeout + ? SelectionOverlay.fadeDuration + : kDoubleTapTimeout; + + // Double tap to select the text. + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(kDoubleTapTimeout ~/ 2); + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(waitDuration); + + // No error as in https://github.com/flutter/flutter/issues/170521. + + // The Flutter-drawn context menu is shown. The SystemContextMenu is not + // shown because readOnly is true. + expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsOneWidget); + expect(find.byType(SystemContextMenu), findsNothing); + + // Turn off readOnly and hide the context menu. + setState(() { + readOnly = false; + }); + await tester.tap(find.text('Copy')); + await tester.pump(waitDuration); + + expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsNothing); + expect(find.byType(SystemContextMenu), findsNothing); + + // Double tap to show the context menu again. + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(kDoubleTapTimeout ~/ 2); + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(waitDuration); + + // Now iOS is showing the SystemContextMenu while others continue to show + // the Flutter-drawn context menu. + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + expect(find.byType(SystemContextMenu), findsOneWidget); + case TargetPlatform.macOS: + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsOneWidget); + } + }, + variant: TargetPlatformVariant.all(), + skip: kIsWeb, // [intended] on web the browser handles the context menu. + ); } diff --git a/packages/flutter/test/cupertino/text_form_field_row_test.dart b/packages/flutter/test/cupertino/text_form_field_row_test.dart index 3d4cf95e713..9cc9d70e10e 100644 --- a/packages/flutter/test/cupertino/text_form_field_row_test.dart +++ b/packages/flutter/test/cupertino/text_form_field_row_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/src/services/spell_check.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -569,4 +570,86 @@ void main() { variant: TargetPlatformVariant.only(TargetPlatform.iOS), ); }); + + testWidgets( + 'readOnly disallows SystemContextMenu', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/170521. + tester.platformDispatcher.supportsShowingSystemContextMenu = true; + final TextEditingController controller = TextEditingController(text: 'abcdefghijklmnopqr'); + addTearDown(() { + tester.platformDispatcher.resetSupportsShowingSystemContextMenu(); + tester.view.reset(); + controller.dispose(); + }); + + bool readOnly = true; + late StateSetter setState; + + await tester.pumpWidget( + // Don't wrap with the global View so that the change to + // platformDispatcher is read. + wrapWithView: false, + View( + view: tester.view, + child: CupertinoApp( + home: StatefulBuilder( + builder: (BuildContext context, StateSetter setter) { + setState = setter; + return CupertinoTextFormFieldRow(readOnly: readOnly, controller: controller); + }, + ), + ), + ), + ); + + final Duration waitDuration = SelectionOverlay.fadeDuration > kDoubleTapTimeout + ? SelectionOverlay.fadeDuration + : kDoubleTapTimeout; + + // Double tap to select the text. + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(kDoubleTapTimeout ~/ 2); + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(waitDuration); + + // No error as in https://github.com/flutter/flutter/issues/170521. + + // The Flutter-drawn context menu is shown. The SystemContextMenu is not + // shown because readOnly is true. + expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsOneWidget); + expect(find.byType(SystemContextMenu), findsNothing); + + // Turn off readOnly and hide the context menu. + setState(() { + readOnly = false; + }); + await tester.tap(find.text('Copy')); + await tester.pump(waitDuration); + + expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsNothing); + expect(find.byType(SystemContextMenu), findsNothing); + + // Double tap to show the context menu again. + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(kDoubleTapTimeout ~/ 2); + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(waitDuration); + + // Now iOS is showing the SystemContextMenu while others continue to show + // the Flutter-drawn context menu. + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + expect(find.byType(SystemContextMenu), findsOneWidget); + case TargetPlatform.macOS: + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + expect(find.byType(CupertinoAdaptiveTextSelectionToolbar), findsOneWidget); + } + }, + variant: TargetPlatformVariant.all(), + skip: kIsWeb, // [intended] on web the browser handles the context menu. + ); } diff --git a/packages/flutter/test/material/search_anchor_test.dart b/packages/flutter/test/material/search_anchor_test.dart index 3fc7d378508..282cecf39a9 100644 --- a/packages/flutter/test/material/search_anchor_test.dart +++ b/packages/flutter/test/material/search_anchor_test.dart @@ -4158,6 +4158,90 @@ void main() { expect(lastItemBottom, lessThanOrEqualTo(fakeKeyboardTop)); }, ); + + testWidgets( + 'readOnly disallows SystemContextMenu', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/170521. + tester.platformDispatcher.supportsShowingSystemContextMenu = true; + final TextEditingController controller = TextEditingController(text: 'abcdefghijklmnopqr'); + addTearDown(() { + tester.platformDispatcher.resetSupportsShowingSystemContextMenu(); + tester.view.reset(); + controller.dispose(); + }); + + bool readOnly = true; + late StateSetter setState; + + await tester.pumpWidget( + // Don't wrap with the global View so that the change to + // platformDispatcher is read. + wrapWithView: false, + View( + view: tester.view, + child: MaterialApp( + home: Material( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setter) { + setState = setter; + return SearchBar(controller: controller, readOnly: readOnly); + }, + ), + ), + ), + ), + ); + + final Duration waitDuration = SelectionOverlay.fadeDuration > kDoubleTapTimeout + ? SelectionOverlay.fadeDuration + : kDoubleTapTimeout; + + // Double tap to select the text. + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(kDoubleTapTimeout ~/ 2); + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(waitDuration); + + // No error as in https://github.com/flutter/flutter/issues/170521. + + // The Flutter-drawn context menu is shown. The SystemContextMenu is not + // shown because readOnly is true. + expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); + expect(find.byType(SystemContextMenu), findsNothing); + + // Turn off readOnly and hide the context menu. + setState(() { + readOnly = false; + }); + await tester.tap(find.text('Copy')); + await tester.pump(waitDuration); + + expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing); + expect(find.byType(SystemContextMenu), findsNothing); + + // Double tap to show the context menu again. + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(kDoubleTapTimeout ~/ 2); + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(waitDuration); + + // Now iOS is showing the SystemContextMenu while others continue to show + // the Flutter-drawn context menu. + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + expect(find.byType(SystemContextMenu), findsOneWidget); + case TargetPlatform.macOS: + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); + } + }, + variant: TargetPlatformVariant.all(), + skip: kIsWeb, // [intended] on web the browser handles the context menu. + ); } Future checkSearchBarDefaults( diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 1d62639f06e..4b602d5cf55 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -18640,6 +18640,90 @@ void main() { final EditableText editableText = tester.widget(find.byType(EditableText)); expect(editableText.hintLocales, hintLocales); }); + + testWidgets( + 'readOnly disallows SystemContextMenu', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/170521. + tester.platformDispatcher.supportsShowingSystemContextMenu = true; + final TextEditingController controller = TextEditingController(text: 'abcdefghijklmnopqr'); + addTearDown(() { + tester.platformDispatcher.resetSupportsShowingSystemContextMenu(); + tester.view.reset(); + controller.dispose(); + }); + + bool readOnly = true; + late StateSetter setState; + + await tester.pumpWidget( + // Don't wrap with the global View so that the change to + // platformDispatcher is read. + wrapWithView: false, + View( + view: tester.view, + child: MaterialApp( + home: Material( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setter) { + setState = setter; + return TextField(readOnly: readOnly, controller: controller); + }, + ), + ), + ), + ), + ); + + final Duration waitDuration = SelectionOverlay.fadeDuration > kDoubleTapTimeout + ? SelectionOverlay.fadeDuration + : kDoubleTapTimeout; + + // Double tap to select the text. + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(kDoubleTapTimeout ~/ 2); + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(waitDuration); + + // No error as in https://github.com/flutter/flutter/issues/170521. + + // The Flutter-drawn context menu is shown. The SystemContextMenu is not + // shown because readOnly is true. + expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); + expect(find.byType(SystemContextMenu), findsNothing); + + // Turn off readOnly and hide the context menu. + setState(() { + readOnly = false; + }); + await tester.tap(find.text('Copy')); + await tester.pump(waitDuration); + + expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing); + expect(find.byType(SystemContextMenu), findsNothing); + + // Double tap to show the context menu again. + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(kDoubleTapTimeout ~/ 2); + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(waitDuration); + + // Now iOS is showing the SystemContextMenu while others continue to show + // the Flutter-drawn context menu. + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + expect(find.byType(SystemContextMenu), findsOneWidget); + case TargetPlatform.macOS: + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); + } + }, + variant: TargetPlatformVariant.all(), + skip: kIsWeb, // [intended] on web the browser handles the context menu. + ); } /// A Simple widget for testing the obscure text. diff --git a/packages/flutter/test/material/text_form_field_test.dart b/packages/flutter/test/material/text_form_field_test.dart index 631065f62e1..b87c8aa14ad 100644 --- a/packages/flutter/test/material/text_form_field_test.dart +++ b/packages/flutter/test/material/text_form_field_test.dart @@ -1764,4 +1764,88 @@ void main() { variant: TargetPlatformVariant.only(TargetPlatform.iOS), ); }); + + testWidgets( + 'readOnly disallows SystemContextMenu', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/170521. + tester.platformDispatcher.supportsShowingSystemContextMenu = true; + final TextEditingController controller = TextEditingController(text: 'abcdefghijklmnopqr'); + addTearDown(() { + tester.platformDispatcher.resetSupportsShowingSystemContextMenu(); + tester.view.reset(); + controller.dispose(); + }); + + bool readOnly = true; + late StateSetter setState; + + await tester.pumpWidget( + // Don't wrap with the global View so that the change to + // platformDispatcher is read. + wrapWithView: false, + View( + view: tester.view, + child: MaterialApp( + home: Material( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setter) { + setState = setter; + return TextFormField(readOnly: readOnly, controller: controller); + }, + ), + ), + ), + ), + ); + + final Duration waitDuration = SelectionOverlay.fadeDuration > kDoubleTapTimeout + ? SelectionOverlay.fadeDuration + : kDoubleTapTimeout; + + // Double tap to select the text. + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(kDoubleTapTimeout ~/ 2); + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(waitDuration); + + // No error as in https://github.com/flutter/flutter/issues/170521. + + // The Flutter-drawn context menu is shown. The SystemContextMenu is not + // shown because readOnly is true. + expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); + expect(find.byType(SystemContextMenu), findsNothing); + + // Turn off readOnly and hide the context menu. + setState(() { + readOnly = false; + }); + await tester.tap(find.text('Copy')); + await tester.pump(waitDuration); + + expect(find.byType(AdaptiveTextSelectionToolbar), findsNothing); + expect(find.byType(SystemContextMenu), findsNothing); + + // Double tap to show the context menu again. + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(kDoubleTapTimeout ~/ 2); + await tester.tapAt(textOffsetToPosition(tester, 5)); + await tester.pump(waitDuration); + + // Now iOS is showing the SystemContextMenu while others continue to show + // the Flutter-drawn context menu. + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + expect(find.byType(SystemContextMenu), findsOneWidget); + case TargetPlatform.macOS: + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); + } + }, + variant: TargetPlatformVariant.all(), + skip: kIsWeb, // [intended] on web the browser handles the context menu. + ); } diff --git a/packages/flutter/test/widgets/system_context_menu_test.dart b/packages/flutter/test/widgets/system_context_menu_test.dart index 7e0529643c8..5fc4105ce3a 100644 --- a/packages/flutter/test/widgets/system_context_menu_test.dart +++ b/packages/flutter/test/widgets/system_context_menu_test.dart @@ -737,4 +737,162 @@ void main() { expect(diagnosticsNodes.first.name, 'title'); expect(diagnosticsNodes.first.value, title); }); + + testWidgets( + 'when supportsShowingSystemContextMenu is false, isSupported is false', + (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(text: 'one two three'); + addTearDown(controller.dispose); + late BuildContext buildContext; + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + final MediaQueryData mediaQueryData = MediaQuery.of(context); + return MediaQuery( + data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: false), + child: MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + buildContext = context; + return TextField( + controller: controller, + contextMenuBuilder: + (BuildContext context, EditableTextState editableTextState) { + return SystemContextMenu.editableText( + editableTextState: editableTextState, + ); + }, + ); + }, + ), + ), + ), + ); + }, + ), + ); + + expect(SystemContextMenu.isSupported(buildContext), isFalse); + }, + skip: kIsWeb, // [intended] SystemContextMenu is not supported on web. + variant: TargetPlatformVariant.only(TargetPlatform.iOS), + ); + + testWidgets( + 'when supportsShowingSystemContextMenu is true and the platform is iOS, isSupported is true', + (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(text: 'one two three'); + addTearDown(controller.dispose); + late BuildContext buildContext; + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + final MediaQueryData mediaQueryData = MediaQuery.of(context); + return MediaQuery( + data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: true), + child: MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + buildContext = context; + return TextField( + controller: controller, + contextMenuBuilder: + (BuildContext context, EditableTextState editableTextState) { + return SystemContextMenu.editableText( + editableTextState: editableTextState, + ); + }, + ); + }, + ), + ), + ), + ); + }, + ), + ); + + expect(SystemContextMenu.isSupported(buildContext), switch (defaultTargetPlatform) { + TargetPlatform.iOS => isTrue, + _ => isFalse, + }); + }, + skip: kIsWeb, // [intended] SystemContextMenu is not supported on web. + variant: TargetPlatformVariant.all(), + ); + + for (final bool readOnly in [true, false]) { + testWidgets( + 'read only fields do not support the system context menu', + (WidgetTester tester) async { + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + final MediaQueryData mediaQueryData = MediaQuery.of(context); + return MediaQuery( + data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: true), + child: MaterialApp( + home: Scaffold(body: TextField(readOnly: readOnly)), + ), + ); + }, + ), + ); + + final EditableTextState editableTextState = tester.state(find.byType(EditableText)); + expect(SystemContextMenu.isSupportedByField(editableTextState), switch (readOnly) { + true => isFalse, + false => isTrue, + }); + }, + skip: kIsWeb, // [intended] SystemContextMenu is not supported on web. + variant: TargetPlatformVariant.only(TargetPlatform.iOS), + ); + } + + // Regression test for https://github.com/flutter/flutter/issues/170521. + testWidgets( + 'when supportsShowingSystemContextMenu is false, SystemContextMenu throws', + (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(text: 'one two three'); + addTearDown(controller.dispose); + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + final MediaQueryData mediaQueryData = MediaQuery.of(context); + return MediaQuery( + data: mediaQueryData.copyWith(supportsShowingSystemContextMenu: false), + child: MaterialApp( + home: Scaffold( + body: Center( + child: TextField( + controller: controller, + contextMenuBuilder: + (BuildContext context, EditableTextState editableTextState) { + return SystemContextMenu.editableText( + editableTextState: editableTextState, + ); + }, + ), + ), + ), + ), + ); + }, + ), + ); + + expect(find.byType(SystemContextMenu), findsNothing); + + await tester.tap(find.byType(TextField)); + final EditableTextState state = tester.state(find.byType(EditableText)); + expect(state.showToolbar(), true); + await tester.pump(); + + expect(tester.takeException(), isAssertionError); + }, + skip: kIsWeb, // [intended] SystemContextMenu is not supported on web. + ); }