-
Notifications
You must be signed in to change notification settings - Fork 6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ability to disable browser context menu #38682
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both PRs (engine & framework) look good to me overall. They are still in draft mode, so I'll assume that tests will be added later.
@@ -0,0 +1,37 @@ | |||
// Copyright 2013 The Flutter Authors. All rights reserved. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The path of this file needs to be added here:
engine/ci/licenses_golden/licenses_flutter
Lines 1910 to 1914 in eb5c6f0
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/clipboard.dart + ../../../flutter/LICENSE | |
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/color_filter.dart + ../../../flutter/LICENSE | |
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/configuration.dart + ../../../flutter/LICENSE | |
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/dom.dart + ../../../flutter/LICENSE | |
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/embedder.dart + ../../../flutter/LICENSE |
/// | ||
/// Can be re-enabled by calling [enableContextMenu]. | ||
static void disableContextMenu() { | ||
domWindow.addEventListener('contextmenu', _handleContextMenu); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The flutter app could be embedded inside a host element (instead of taking over the entire window). We should add the event listener to the host element. cc @ditman for the right way to do this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The contextmenu
event bubbles (spec), so this should be enough?
If this needs to be aware of the embedding, the enable/disable methods could be added to the EmbeddingStrategy
abstract class (and its implementations):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
contextmenu
event bubbles (spec), so this should be enough?
Yes, but it's more than enough :) It disables the context menu for the entire page, which we don't want in the host element mode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're very right :P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the advice here! I've moved the enable/disable methods to the embedding strategy and made them accessible via FlutterViewEmbedder.
This isn't possible because the entire app could be rendered on a canvas element. To achieve this, you want the pointer event listener on the framework side to decide whether it wants to show a context menu or not (e.g. based on which widget was hit tested). And it should immediately tell the engine that a context menu shouldn't be shown for this event. |
Thanks for the review! Understood about specific widget subtrees. I just played around with using the API in this PR to quickly disable and then reenable the context menu for a specific GestureDetector in the framework, and it worked, which is a good endorsement for this API. I'm going to go ahead with cleaning these two PRs up and writing tests for them when I'm back next week, and I'll let you know when they're ready for a real review. |
c9ca134
to
383427d
Compare
383427d
to
4fd56bd
Compare
expect(strategy.contextMenuEnabled, isTrue); | ||
|
||
strategy.disableContextMenu(); | ||
expect(strategy.contextMenuEnabled, isFalse); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there anything else I could expect here, like could I somehow verify that the event listener was added to the dom element?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could add your own contextmenu
Event handler to the root DOM element of the strategy in the test, and check if isDefaultPrevented
has been set to true
. (IIRC, once you prevent the default in a handler, the event will be modified to set defaultPrevented
to true.)
In order to programmatically trigger the event, you should be able to dispatchEvent
a contextmenu
event using the root DOM element of the strategy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are some examples in our code base. E.g.
expect(event.defaultPrevented, isFalse); |
You don't have to create an event listener on the root element. Just check defaultPrevented
on the event you dispatched.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That worked, thank you all for the guidance here! I think the tests are much more robust this way, and I don't have to use @visibleForTest
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just check defaultPrevented on the event you dispatched.
Damn this approach is good! Thanks for the tests @justinmc!
0b12dee
to
a2c5a88
Compare
@@ -531,6 +530,21 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { | |||
textEditing.channel.handleTextInput(data, callback); | |||
return; | |||
|
|||
case 'flutter/contextmenu': |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've omitted any web or browser namespacing (not "flutter/webcontextmenu") because other methods don't seem to include platform names like that. And maybe in the future this could be used on other platforms.
In the framework PR I'll make the API specific (BrowserContextMenu.disable or something like that).
…119081) * 22084db7a Remove unnecessary null checks in doc snippet (flutter/engine#39071) * f91890636 Roll Skia from f6a5c806294d to 1ccf2093cfa9 (2 revisions) (flutter/engine#39028) * f02a70492 Roll Fuchsia Mac SDK from rQtxqj3gePeR-nTLv... to HxpwvvbQdk54L6_8q... (flutter/engine#39075) * 42eec6280 [Impeller] uniform offsets account for size (flutter/engine#39077) * b6a348a82 Ability to disable browser context menu (flutter/engine#38682) * 1b51696eb Roll Skia from 1ccf2093cfa9 to 0328e12ab195 (59 revisions) (flutter/engine#39078) * d83a705ef [embedder] Fix getting vkGetInstanceProcAddr (flutter/engine#39051) * 973b01c40 Fix doc analyzer breakage (flutter/engine#39082) * 55e9eafab Roll Dart SDK from 107a1280a61f to d1a0e860679e (2 revisions) (flutter/engine#39084) * fa07c546c Roll Skia from 0328e12ab195 to 50d78376d30c (3 revisions) (flutter/engine#39086) * 17abf1a9e Roll Fuchsia Linux SDK from GLRbnjiO5SbZKX-Us... to dWbkAZchFHtZE9Wt_... (flutter/engine#39087) * 5c46d75f7 Roll Skia from 50d78376d30c to 90fda2e72314 (4 revisions) (flutter/engine#39089) * 5b4e331a6 Add mmap dependency to flutter_frontend_server (flutter/engine#39090) * 7f38d0d4f Migrate `@FfiNative` to `@Native` (flutter/engine#39034) * 71ee5f19b Remove superfluous words from comments (flutter/engine#39068)
Adds platform channel methods that disable/enable the context menu on web.
By default, Flutter web uses the browser's built-in context menu. <img width="200" src="https://github.com/flutter/flutter/assets/389558/990f99cb-bc38-40f1-9e88-8839bc342da5" /> As of [recently](flutter/engine#38682), it's possible to use a Flutter-rendered context menu like the other platforms. ```dart void main() { runApp(const MyApp()); BrowserContextMenu.disableContextMenu(); } ``` But there is a bug (#129692) that the Paste button is missing and never shows up. <img width="284" alt="Screenshot 2023-08-07 at 2 39 03 PM" src="https://github.com/flutter/flutter/assets/389558/f632be25-28b1-4e2e-98f7-3bb443f077df"> The reason why it's missing is that Flutter first checks if there is any pasteable text on the clipboard before deciding to show the Paste button using the `hasStrings` platform channel method, but that was never implemented for web ([original hasStrings PR](#87678)). So let's just implement hasStrings for web? No, because Chrome shows a permissions prompt when the clipboard is accessed, and there is [no browser clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API) to avoid it. The prompt will show immediately when the EditableText is built, not just when the Paste button is pressed. <img width="200" src="https://github.com/flutter/flutter/assets/389558/5abdb160-1b13-4f1a-87e1-4653ca19d73e" /> ### This PR's solution Instead, before implementing hasStrings for web, this PR disables the hasStrings check for web. The result is that users will always see a paste button, even in the (unlikely) case that they have nothing pasteable on the clipboard. However, they will not see a permissions dialog until they actually click the Paste button. Subsequent pastes don't show the permission dialog. <details> <summary>Video of final behavior with this PR</summary> https://github.com/flutter/flutter/assets/389558/ed16c925-8111-44a7-99e8-35a09d682748 </details> I think this will be the desired behavior for the vast majority of app developers. Those that want different behavior can use hasStrings themselves, which will be implemented in flutter/engine#43360. ### References Fixes #129692 Engine PR to be merged after this: flutter/engine#43360
The goal of this PR is to create a way to disable the browser-rendered context menu on web.
The main reason that someone might want to do this is to use a custom Flutter-rendered context menu instead, which this PR will unblock.
After this PR:
Screen.Recording.2023-01-06.at.4.34.53.PM.mov
The current, basic approach
I'm just calling
event.preventDefault()
oncontextmenu
events at the rootdomWindow
level. I've tied this to platform channel methods that could be exposed in the framework.Alternative, more fully featured approach
It might be nice if, instead of toggling the context menus for the whole app, users could toggle it for a widget subtree. Would it be at all possible to do this? I guess there would have to be a specific DOM element for every given widget that we could add/remove the listener on.
With either approach, please excuse my lack of web engine knowledge and let me know if I'm on the right track!
References
Part of: flutter/flutter#78671
Framework PR: flutter/flutter#118194
Example code