mirror of
https://github.com/flutter/flutter.git
synced 2026-02-15 23:33:36 +08:00
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
This commit is contained in:
parent
0d999f39e9
commit
f16f6d7f99
@ -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);
|
||||
|
||||
@ -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<String> {
|
||||
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);
|
||||
|
||||
@ -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<SearchBar> {
|
||||
child: Semantics(
|
||||
inputType: SemanticsInputType.search,
|
||||
child: TextField(
|
||||
readOnly: widget.readOnly,
|
||||
autofocus: widget.autoFocus,
|
||||
onTap: widget.onTap,
|
||||
onTapAlwaysCalled: true,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<String> {
|
||||
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);
|
||||
|
||||
@ -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].
|
||||
|
||||
@ -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.
|
||||
);
|
||||
}
|
||||
|
||||
@ -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.
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<void> checkSearchBarDefaults(
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 <bool>[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<EditableTextState>(find.byType(EditableText));
|
||||
expect(state.showToolbar(), true);
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.takeException(), isAssertionError);
|
||||
},
|
||||
skip: kIsWeb, // [intended] SystemContextMenu is not supported on web.
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user