Ability to disable the browser's context menu on web (#118194)
Enables custom context menus on web
This commit is contained in:
parent
530c3f2d13
commit
17eb2e8aeb
@ -14,6 +14,7 @@ export 'src/services/asset_bundle.dart';
|
|||||||
export 'src/services/autofill.dart';
|
export 'src/services/autofill.dart';
|
||||||
export 'src/services/binary_messenger.dart';
|
export 'src/services/binary_messenger.dart';
|
||||||
export 'src/services/binding.dart';
|
export 'src/services/binding.dart';
|
||||||
|
export 'src/services/browser_context_menu.dart';
|
||||||
export 'src/services/clipboard.dart';
|
export 'src/services/clipboard.dart';
|
||||||
export 'src/services/debug.dart';
|
export 'src/services/debug.dart';
|
||||||
export 'src/services/deferred_component.dart';
|
export 'src/services/deferred_component.dart';
|
||||||
|
83
packages/flutter/lib/src/services/browser_context_menu.dart
Normal file
83
packages/flutter/lib/src/services/browser_context_menu.dart
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import 'system_channels.dart';
|
||||||
|
|
||||||
|
/// Controls the browser's context menu on the web platform.
|
||||||
|
///
|
||||||
|
/// The context menu is the menu that appears on right clicking or selecting
|
||||||
|
/// text in the browser, for example.
|
||||||
|
///
|
||||||
|
/// On web, by default, the browser's context menu is enabled and Flutter's
|
||||||
|
/// context menus are hidden.
|
||||||
|
///
|
||||||
|
/// On all non-web platforms, this does nothing.
|
||||||
|
class BrowserContextMenu {
|
||||||
|
BrowserContextMenu._();
|
||||||
|
|
||||||
|
static final BrowserContextMenu _instance = BrowserContextMenu._();
|
||||||
|
|
||||||
|
/// Whether showing the browser's context menu is enabled.
|
||||||
|
///
|
||||||
|
/// When true, any event that the browser typically uses to trigger its
|
||||||
|
/// context menu (e.g. right click) will do so. When false, the browser's
|
||||||
|
/// context menu will not show.
|
||||||
|
///
|
||||||
|
/// It's possible for this to be true but for the browser's context menu to
|
||||||
|
/// not show due to direct manipulation of the DOM. For example, handlers for
|
||||||
|
/// the browser's `contextmenu` event could be added/removed in the browser's
|
||||||
|
/// JavaScript console, and this boolean wouldn't know about it. This boolean
|
||||||
|
/// only indicates the results of calling [disableContextMenu] and
|
||||||
|
/// [enableContextMenu] here.
|
||||||
|
///
|
||||||
|
/// Defaults to true.
|
||||||
|
static bool get enabled => _instance._enabled;
|
||||||
|
|
||||||
|
bool _enabled = true;
|
||||||
|
|
||||||
|
final MethodChannel _channel = SystemChannels.contextMenu;
|
||||||
|
|
||||||
|
/// Disable the browser's context menu.
|
||||||
|
///
|
||||||
|
/// By default, when the app starts, the browser's context menu is already
|
||||||
|
/// enabled.
|
||||||
|
///
|
||||||
|
/// This is an asynchronous action. The context menu can be considered to be
|
||||||
|
/// disabled at the time that the Future resolves. [enabled] won't reflect the
|
||||||
|
/// change until that time.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [enableContextMenu], which performs the opposite operation.
|
||||||
|
static Future<void> disableContextMenu() {
|
||||||
|
assert(kIsWeb, 'This has no effect on platforms other than web.');
|
||||||
|
return _instance._channel.invokeMethod<void>(
|
||||||
|
'disableContextMenu',
|
||||||
|
).then((_) {
|
||||||
|
_instance._enabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable the browser's context menu.
|
||||||
|
///
|
||||||
|
/// By default, when the app starts, the browser's context menu is already
|
||||||
|
/// enabled. Typically this method would be called after first calling
|
||||||
|
/// [disableContextMenu].
|
||||||
|
///
|
||||||
|
/// This is an asynchronous action. The context menu can be considered to be
|
||||||
|
/// enabled at the time that the Future resolves. [enabled] won't reflect the
|
||||||
|
/// change until that time.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [disableContextMenu], which performs the opposite operation.
|
||||||
|
static Future<void> enableContextMenu() {
|
||||||
|
assert(kIsWeb, 'This has no effect on platforms other than web.');
|
||||||
|
return _instance._channel.invokeMethod<void>(
|
||||||
|
'enableContextMenu',
|
||||||
|
).then((_) {
|
||||||
|
_instance._enabled = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -465,4 +465,17 @@ class SystemChannels {
|
|||||||
///
|
///
|
||||||
/// * [DefaultPlatformMenuDelegate], which uses this channel.
|
/// * [DefaultPlatformMenuDelegate], which uses this channel.
|
||||||
static const MethodChannel menu = OptionalMethodChannel('flutter/menu');
|
static const MethodChannel menu = OptionalMethodChannel('flutter/menu');
|
||||||
|
|
||||||
|
/// A [MethodChannel] for configuring the browser's context menu on web.
|
||||||
|
///
|
||||||
|
/// The following outgoing methods are defined for this channel (invoked using
|
||||||
|
/// [OptionalMethodChannel.invokeMethod]):
|
||||||
|
///
|
||||||
|
/// * `enableContextMenu`: enables the browser's context menu. When a Flutter
|
||||||
|
/// app starts, the browser's context menu is already enabled.
|
||||||
|
/// * `disableContextMenu`: disables the browser's context menu.
|
||||||
|
static const MethodChannel contextMenu = OptionalMethodChannel(
|
||||||
|
'flutter/contextmenu',
|
||||||
|
JSONMethodCodec(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1893,7 +1893,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
final GlobalKey _editableKey = GlobalKey();
|
final GlobalKey _editableKey = GlobalKey();
|
||||||
|
|
||||||
/// Detects whether the clipboard can paste.
|
/// Detects whether the clipboard can paste.
|
||||||
final ClipboardStatusNotifier? clipboardStatus = kIsWeb ? null : ClipboardStatusNotifier();
|
final ClipboardStatusNotifier clipboardStatus = ClipboardStatusNotifier();
|
||||||
|
|
||||||
TextInputConnection? _textInputConnection;
|
TextInputConnection? _textInputConnection;
|
||||||
bool get _hasInputConnection => _textInputConnection?.attached ?? false;
|
bool get _hasInputConnection => _textInputConnection?.attached ?? false;
|
||||||
@ -1996,8 +1996,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
return widget.toolbarOptions.paste && !widget.readOnly;
|
return widget.toolbarOptions.paste && !widget.readOnly;
|
||||||
}
|
}
|
||||||
return !widget.readOnly
|
return !widget.readOnly
|
||||||
&& (clipboardStatus == null
|
&& (clipboardStatus.value == ClipboardStatus.pasteable);
|
||||||
|| clipboardStatus!.value == ClipboardStatus.pasteable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -2074,7 +2073,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clipboardStatus?.update();
|
clipboardStatus.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cut current selection to [Clipboard].
|
/// Cut current selection to [Clipboard].
|
||||||
@ -2099,7 +2098,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
});
|
});
|
||||||
hideToolbar();
|
hideToolbar();
|
||||||
}
|
}
|
||||||
clipboardStatus?.update();
|
clipboardStatus.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paste text from [Clipboard].
|
/// Paste text from [Clipboard].
|
||||||
@ -2285,7 +2284,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
},
|
},
|
||||||
type: ContextMenuButtonType.copy,
|
type: ContextMenuButtonType.copy,
|
||||||
),
|
),
|
||||||
if (toolbarOptions.paste && clipboardStatus != null && pasteEnabled)
|
if (toolbarOptions.paste && pasteEnabled)
|
||||||
ContextMenuButtonItem(
|
ContextMenuButtonItem(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
pasteText(SelectionChangedCause.toolbar);
|
pasteText(SelectionChangedCause.toolbar);
|
||||||
@ -2386,7 +2385,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
/// button Widgets for the current platform given [ContextMenuButtonItem]s.
|
/// button Widgets for the current platform given [ContextMenuButtonItem]s.
|
||||||
List<ContextMenuButtonItem> get contextMenuButtonItems {
|
List<ContextMenuButtonItem> get contextMenuButtonItems {
|
||||||
return buttonItemsForToolbarOptions() ?? EditableText.getEditableButtonItems(
|
return buttonItemsForToolbarOptions() ?? EditableText.getEditableButtonItems(
|
||||||
clipboardStatus: clipboardStatus?.value,
|
clipboardStatus: clipboardStatus.value,
|
||||||
onCopy: copyEnabled
|
onCopy: copyEnabled
|
||||||
? () => copySelection(SelectionChangedCause.toolbar)
|
? () => copySelection(SelectionChangedCause.toolbar)
|
||||||
: null,
|
: null,
|
||||||
@ -2407,7 +2406,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
clipboardStatus?.addListener(_onChangedClipboardStatus);
|
clipboardStatus.addListener(_onChangedClipboardStatus);
|
||||||
widget.controller.addListener(_didChangeTextEditingValue);
|
widget.controller.addListener(_didChangeTextEditingValue);
|
||||||
widget.focusNode.addListener(_handleFocusChanged);
|
widget.focusNode.addListener(_handleFocusChanged);
|
||||||
_scrollController.addListener(_onEditableScroll);
|
_scrollController.addListener(_onEditableScroll);
|
||||||
@ -2531,8 +2530,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
final bool canPaste = widget.selectionControls is TextSelectionHandleControls
|
final bool canPaste = widget.selectionControls is TextSelectionHandleControls
|
||||||
? pasteEnabled
|
? pasteEnabled
|
||||||
: widget.selectionControls?.canPaste(this) ?? false;
|
: widget.selectionControls?.canPaste(this) ?? false;
|
||||||
if (widget.selectionEnabled && pasteEnabled && clipboardStatus != null && canPaste) {
|
if (widget.selectionEnabled && pasteEnabled && canPaste) {
|
||||||
clipboardStatus!.update();
|
clipboardStatus.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2553,8 +2552,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
_selectionOverlay = null;
|
_selectionOverlay = null;
|
||||||
widget.focusNode.removeListener(_handleFocusChanged);
|
widget.focusNode.removeListener(_handleFocusChanged);
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
clipboardStatus?.removeListener(_onChangedClipboardStatus);
|
clipboardStatus.removeListener(_onChangedClipboardStatus);
|
||||||
clipboardStatus?.dispose();
|
clipboardStatus.dispose();
|
||||||
_cursorVisibilityNotifier.dispose();
|
_cursorVisibilityNotifier.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
assert(_batchEditDepth <= 0, 'unfinished batch edits: $_batchEditDepth');
|
assert(_batchEditDepth <= 0, 'unfinished batch edits: $_batchEditDepth');
|
||||||
@ -3688,17 +3687,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
@override
|
@override
|
||||||
bool showToolbar() {
|
bool showToolbar() {
|
||||||
// Web is using native dom elements to enable clipboard functionality of the
|
// Web is using native dom elements to enable clipboard functionality of the
|
||||||
// toolbar: copy, paste, select, cut. It might also provide additional
|
// context menu: copy, paste, select, cut. It might also provide additional
|
||||||
// functionality depending on the browser (such as translate). Due to this
|
// functionality depending on the browser (such as translate). Due to this,
|
||||||
// we should not show a Flutter toolbar for the editable text elements.
|
// we should not show a Flutter toolbar for the editable text elements
|
||||||
if (kIsWeb) {
|
// unless the browser's context menu is explicitly disabled.
|
||||||
|
if (kIsWeb && BrowserContextMenu.enabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_selectionOverlay == null) {
|
if (_selectionOverlay == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
clipboardStatus?.update();
|
clipboardStatus.update();
|
||||||
_selectionOverlay!.showToolbar();
|
_selectionOverlay!.showToolbar();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -3912,7 +3912,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
&& (widget.selectionControls is TextSelectionHandleControls
|
&& (widget.selectionControls is TextSelectionHandleControls
|
||||||
? pasteEnabled
|
? pasteEnabled
|
||||||
: pasteEnabled && (widget.selectionControls?.canPaste(this) ?? false))
|
: pasteEnabled && (widget.selectionControls?.canPaste(this) ?? false))
|
||||||
&& (clipboardStatus == null || clipboardStatus!.value == ClipboardStatus.pasteable)
|
&& (clipboardStatus.value == ClipboardStatus.pasteable)
|
||||||
? () {
|
? () {
|
||||||
controls?.handlePaste(this);
|
controls?.handlePaste(this);
|
||||||
pasteText(SelectionChangedCause.toolbar);
|
pasteText(SelectionChangedCause.toolbar);
|
||||||
|
@ -11914,7 +11914,7 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets('Web does not check the clipboard status', (WidgetTester tester) async {
|
testWidgets('clipboard status is checked via hasStrings without getting the full clipboard contents', (WidgetTester tester) async {
|
||||||
final TextEditingController controller = TextEditingController(
|
final TextEditingController controller = TextEditingController(
|
||||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||||
);
|
);
|
||||||
@ -11958,14 +11958,8 @@ void main() {
|
|||||||
// getData is not called unless something is pasted. hasStrings is used to
|
// getData is not called unless something is pasted. hasStrings is used to
|
||||||
// check the status of the clipboard.
|
// check the status of the clipboard.
|
||||||
expect(calledGetData, false);
|
expect(calledGetData, false);
|
||||||
if (kIsWeb) {
|
|
||||||
// hasStrings is not checked because web doesn't show a custom text
|
|
||||||
// selection menu.
|
|
||||||
expect(calledHasStrings, false);
|
|
||||||
} else {
|
|
||||||
// hasStrings is checked in order to decide if the content can be pasted.
|
// hasStrings is checked in order to decide if the content can be pasted.
|
||||||
expect(calledHasStrings, true);
|
expect(calledHasStrings, true);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('TextField changes mouse cursor when hovered', (WidgetTester tester) async {
|
testWidgets('TextField changes mouse cursor when hovered', (WidgetTester tester) async {
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
final List<MethodCall> log = <MethodCall>[];
|
||||||
|
|
||||||
|
Future<void> verify(AsyncCallback test, List<Object> expectations) async {
|
||||||
|
log.clear();
|
||||||
|
await test();
|
||||||
|
expect(log, expectations);
|
||||||
|
}
|
||||||
|
|
||||||
|
group('not on web', () {
|
||||||
|
test('disableContextMenu asserts', () async {
|
||||||
|
try {
|
||||||
|
BrowserContextMenu.disableContextMenu();
|
||||||
|
} catch (error) {
|
||||||
|
expect(error, isAssertionError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('enableContextMenu asserts', () async {
|
||||||
|
try {
|
||||||
|
BrowserContextMenu.enableContextMenu();
|
||||||
|
} catch (error) {
|
||||||
|
expect(error, isAssertionError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
skip: kIsWeb, // [intended]
|
||||||
|
);
|
||||||
|
|
||||||
|
group('on web', () {
|
||||||
|
group('disableContextMenu', () {
|
||||||
|
// Make sure the context menu is enabled (default) after the test.
|
||||||
|
tearDown(() async {
|
||||||
|
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, (MethodCall methodCall) {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
await BrowserContextMenu.enableContextMenu();
|
||||||
|
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('disableContextMenu calls its platform channel method', () async {
|
||||||
|
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, (MethodCall methodCall) async {
|
||||||
|
log.add(methodCall);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
await verify(BrowserContextMenu.disableContextMenu, <Object>[
|
||||||
|
isMethodCall('disableContextMenu', arguments: null),
|
||||||
|
]);
|
||||||
|
|
||||||
|
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('enableContextMenu', () {
|
||||||
|
test('enableContextMenu calls its platform channel method', () async {
|
||||||
|
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, (MethodCall methodCall) async {
|
||||||
|
log.add(methodCall);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
await verify(BrowserContextMenu.enableContextMenu, <Object>[
|
||||||
|
isMethodCall('enableContextMenu', arguments: null),
|
||||||
|
]);
|
||||||
|
|
||||||
|
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
skip: !kIsWeb, // [intended]
|
||||||
|
);
|
||||||
|
}
|
@ -2,7 +2,6 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
|
||||||
import 'dart:convert' show jsonDecode;
|
import 'dart:convert' show jsonDecode;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
@ -1488,13 +1488,14 @@ void main() {
|
|||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Toolbar is not used in Flutter Web. Skip this check.
|
// Toolbar is not used in Flutter Web unless the browser context menu is
|
||||||
///
|
// explicitly disabled. Skip this check.
|
||||||
/// Web is using native DOM elements (it is also used as platform input)
|
//
|
||||||
/// to enable clipboard functionality of the toolbar: copy, paste, select,
|
// Web is using native DOM elements (it is also used as platform input)
|
||||||
/// cut. It might also provide additional functionality depending on the
|
// to enable clipboard functionality of the toolbar: copy, paste, select,
|
||||||
/// browser (such as translation). Due to this, in browsers, we should not
|
// cut. It might also provide additional functionality depending on the
|
||||||
/// show a Flutter toolbar for the editable text elements.
|
// browser (such as translation). Due to this, in browsers, we should not
|
||||||
|
// show a Flutter toolbar for the editable text elements.
|
||||||
testWidgets('can show toolbar when there is text and a selection', (WidgetTester tester) async {
|
testWidgets('can show toolbar when there is text and a selection', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
@ -1542,6 +1543,69 @@ void main() {
|
|||||||
expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget);
|
expect(find.text('Paste'), kIsWeb ? findsNothing : findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('BrowserContextMenu', () {
|
||||||
|
setUp(() async {
|
||||||
|
SystemChannels.contextMenu.setMockMethodCallHandler((MethodCall call) {
|
||||||
|
// Just complete successfully, so that BrowserContextMenu thinks that
|
||||||
|
// the engine successfully received its call.
|
||||||
|
return Future<void>.value();
|
||||||
|
});
|
||||||
|
await BrowserContextMenu.disableContextMenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await BrowserContextMenu.enableContextMenu();
|
||||||
|
SystemChannels.contextMenu.setMockMethodCallHandler(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('web can show toolbar when the browser context menu is disabled', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: EditableText(
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
style: textStyle,
|
||||||
|
cursorColor: cursorColor,
|
||||||
|
selectionControls: materialTextSelectionControls,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final EditableTextState state =
|
||||||
|
tester.state<EditableTextState>(find.byType(EditableText));
|
||||||
|
|
||||||
|
// Can't show the toolbar when there's no focus.
|
||||||
|
expect(state.showToolbar(), false);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Paste'), findsNothing);
|
||||||
|
|
||||||
|
// Can show the toolbar when focused even though there's no text.
|
||||||
|
state.renderEditable.selectWordsInRange(
|
||||||
|
from: Offset.zero,
|
||||||
|
cause: SelectionChangedCause.tap,
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
expect(state.showToolbar(), isTrue);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Paste'), findsOneWidget);
|
||||||
|
|
||||||
|
// Hide the menu again.
|
||||||
|
state.hideToolbar();
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Paste'), findsNothing);
|
||||||
|
|
||||||
|
// Can show the menu with text and a selection.
|
||||||
|
controller.text = 'blah';
|
||||||
|
await tester.pump();
|
||||||
|
expect(state.showToolbar(), isTrue);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Paste'), findsOneWidget);
|
||||||
|
},
|
||||||
|
skip: !kIsWeb, // [intended]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('can hide toolbar with DismissIntent', (WidgetTester tester) async {
|
testWidgets('can hide toolbar with DismissIntent', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user