Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -2042,7 +2042,9 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/key_map.g.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/layers.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/mouse/context_menu.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/mouse/cursor.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/mouse/prevent_default.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/navigation/history.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/noto_font.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/noto_font_encoding.dart + ../../../flutter/LICENSE
Expand Down Expand Up @@ -4795,7 +4797,9 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/key_map.g.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/layers.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse/context_menu.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse/cursor.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse/prevent_default.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/navigation/history.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/noto_font.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/noto_font_encoding.dart
Expand Down
2 changes: 2 additions & 0 deletions lib/web_ui/lib/src/engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ export 'engine/js_interop/js_typed_data.dart';
export 'engine/key_map.g.dart';
export 'engine/keyboard_binding.dart';
export 'engine/layers.dart';
export 'engine/mouse/context_menu.dart';
export 'engine/mouse/cursor.dart';
export 'engine/mouse/prevent_default.dart';
export 'engine/navigation/history.dart';
export 'engine/noto_font.dart';
export 'engine/noto_font_encoding.dart';
Expand Down
14 changes: 0 additions & 14 deletions lib/web_ui/lib/src/engine/embedder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -318,20 +318,6 @@ class FlutterViewEmbedder {
assert(element.parentNode == _resourcesHost);
element.remove();
}

/// Disables the browser's context menu for this part of the DOM.
///
/// By default, when a Flutter web app starts, the context menu is enabled.
///
/// Can be re-enabled by calling [enableContextMenu].
void disableContextMenu() => _embeddingStrategy.disableContextMenu();

/// Enables the browser's context menu for this part of the DOM.
///
/// By default, when a Flutter web app starts, the context menu is already
/// enabled. Typically, this method would be used after calling
/// [disableContextMenu] to first disable it.
void enableContextMenu() => _embeddingStrategy.enableContextMenu();
}

/// The embedder singleton.
Expand Down
42 changes: 42 additions & 0 deletions lib/web_ui/lib/src/engine/mouse/context_menu.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2013 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 '../dom.dart';
import 'prevent_default.dart';

/// Controls the browser's context menu in the given [element].
class ContextMenu {
ContextMenu(this.element);

final DomElement element;

/// False when the context menu has been disabled, otherwise true.
bool _enabled = true;

/// Disables the browser's context menu for this [element].
///
/// By default, when a Flutter web app starts, the context menu is enabled.
///
/// Can be re-enabled by calling [enable].
void disable() {
if (!_enabled) {
return;
}
_enabled = false;
element.addEventListener('contextmenu', preventDefaultListener);
}

/// Enables the browser's context menu for this [element].
///
/// By default, when a Flutter web app starts, the context menu is already
/// enabled. Typically, this method would be used after calling
/// [disable] to first disable it.
void enable() {
if (_enabled) {
return;
}
_enabled = true;
element.removeEventListener('contextmenu', preventDefaultListener);
}
}
10 changes: 10 additions & 0 deletions lib/web_ui/lib/src/engine/mouse/prevent_default.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright 2013 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 '../dom.dart';

/// Listener for DOM events that prevents the default browser behavior.
final DomEventListener preventDefaultListener = createDomEventListener((DomEvent event) {
event.preventDefault();
});
4 changes: 2 additions & 2 deletions lib/web_ui/lib/src/engine/platform_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -566,11 +566,11 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
final MethodCall decoded = codec.decodeMethodCall(data);
switch (decoded.method) {
case 'enableContextMenu':
flutterViewEmbedder.enableContextMenu();
implicitView!.contextMenu.enable();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In all these I'd do implicitView?.contextMenu... because we really don't mind if the implicitView exists or not, or if this call succeeds or fails :)

Also, I find it weird that this contextMenu messages don't have a target viewId.

replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
return;
case 'disableContextMenu':
flutterViewEmbedder.disableContextMenu();
implicitView!.contextMenu.disable();
replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
return;
}
Expand Down
5 changes: 2 additions & 3 deletions lib/web_ui/lib/src/engine/text_editing/text_editing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:ui/ui.dart' as ui;
import '../browser_detection.dart';
import '../dom.dart';
import '../embedder.dart';
import '../mouse/prevent_default.dart';
import '../platform_dispatcher.dart';
import '../safe_browser_api.dart';
import '../semantics.dart';
Expand Down Expand Up @@ -210,9 +211,7 @@ class EngineAutofillForm {
formElement.noValidate = true;
formElement.method = 'post';
formElement.action = '#';
formElement.addEventListener('submit', createDomEventListener((DomEvent e) {
e.preventDefault();
}));
formElement.addEventListener('submit', preventDefaultListener);

// We need to explicitly disable pointer events on the form in Safari Desktop,
// so that we don't have pointer event collisions if users hover over or click
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,6 @@ class CustomElementEmbeddingStrategy extends EmbeddingStrategy {
registerElementForCleanup(resourceHost);
}

@override
void disableContextMenu() => disableContextMenuOn(_hostElement);

@override
void enableContextMenu() => enableContextMenuOn(_hostElement);

void _setHostAttribute(String name, String value) {
_hostElement.setAttribute(name, value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import 'full_page_embedding_strategy.dart';
/// * [CustomElementEmbeddingStrategy] - Flutter is rendered inside a custom host
/// element, provided by the web app programmer through the engine
/// initialization.
abstract class EmbeddingStrategy with _ContextMenu {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

abstract class EmbeddingStrategy {
EmbeddingStrategy() {
// Initialize code to handle hot-restart (debug only).
assert(() {
Expand Down Expand Up @@ -56,71 +56,3 @@ abstract class EmbeddingStrategy with _ContextMenu {
_hotRestartCache?.registerElement(element);
}
}

/// Provides functionality to disable and enable the browser's context menu.
mixin _ContextMenu {
/// False when the context menu has been disabled, otherwise true.
bool _contextMenuEnabled = true;

/// Listener for contextmenu events that prevents the browser's context menu
/// from being shown.
final DomEventListener _disablingContextMenuListener = createDomEventListener((DomEvent event) {
event.preventDefault();
});

/// Disables the browser's context menu for this part of the DOM.
///
/// By default, when a Flutter web app starts, the context menu is enabled.
///
/// Can be re-enabled by calling [enableContextMenu].
///
/// See also:
///
/// * [disableContextMenuOn], which is like this but takes the relevant
/// [DomElement] as a parameter.
void disableContextMenu();

/// Disables the browser's context menu for the given [DomElement].
///
/// See also:
///
/// * [disableContextMenu], which is like this but is not passed a
/// [DomElement].
@protected
void disableContextMenuOn(DomEventTarget element) {
if (!_contextMenuEnabled) {
return;
}

element.addEventListener('contextmenu', _disablingContextMenuListener);
_contextMenuEnabled = false;
}

/// Enables the browser's context menu for this part of the DOM.
///
/// By default, when a Flutter web app starts, the context menu is already
/// enabled. Typically, this method would be used after calling
/// [disableContextMenu] to first disable it.
///
/// See also:
///
/// * [enableContextMenuOn], which is like this but takes the relevant
/// [DomElement] as a parameter.
void enableContextMenu();

/// Enables the browser's context menu for the given [DomElement].
///
/// See also:
///
/// * [enableContextMenu], which is like this but is not passed a
/// [DomElement].
@protected
void enableContextMenuOn(DomEventTarget element) {
if (_contextMenuEnabled) {
return;
}

element.removeEventListener('contextmenu', _disablingContextMenuListener);
_contextMenuEnabled = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,6 @@ class FullPageEmbeddingStrategy extends EmbeddingStrategy {
registerElementForCleanup(resourceHost);
}

@override
void disableContextMenu() => disableContextMenuOn(domWindow);

@override
void enableContextMenu() => enableContextMenuOn(domWindow);

void _setHostAttribute(String name, String value) {
domDocument.body!.setAttribute(name, value);
}
Expand Down
5 changes: 5 additions & 0 deletions lib/web_ui/lib/src/engine/window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import '../engine.dart' show DimensionsProvider, registerHotRestartListener, ren
import 'display.dart';
import 'dom.dart';
import 'embedder.dart';
import 'mouse/context_menu.dart';
import 'mouse/cursor.dart';
import 'navigation/history.dart';
import 'platform_dispatcher.dart';
Expand All @@ -36,6 +37,7 @@ const int kImplicitViewId = 0;
/// In addition to everything defined in [ui.FlutterView], this class adds
/// a few web-specific properties.
abstract interface class EngineFlutterView extends ui.FlutterView {
ContextMenu get contextMenu;
MouseCursor get mouseCursor;
DomElement get rootElement;
}
Expand Down Expand Up @@ -67,6 +69,9 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow implements EngineFlu
@override
late final MouseCursor mouseCursor = MouseCursor(rootElement);

@override
late final ContextMenu contextMenu = ContextMenu(rootElement);

@override
DomElement get rootElement => flutterViewEmbedder.flutterViewElement;

Expand Down
104 changes: 104 additions & 0 deletions lib/web_ui/test/engine/mouse/context_menu_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2013 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:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';

void main() {
internalBootstrapBrowserTest(() => testMain);
}

void testMain() {
group('$ContextMenu', () {
test('can disable context menu', () {
final DomElement rootViewElement = createDomElement('div');
final ContextMenu contextMenu = ContextMenu(rootViewElement);

// When the app starts, contextmenu events are not prevented.
DomEvent event = createDomEvent('Event', 'contextmenu');
expect(event.defaultPrevented, isFalse);
rootViewElement.dispatchEvent(event);
expect(event.defaultPrevented, isFalse);

// Disabling the context menu causes contextmenu events to be prevented.
contextMenu.disable();
event = createDomEvent('Event', 'contextmenu');
expect(event.defaultPrevented, isFalse);
rootViewElement.dispatchEvent(event);
expect(event.defaultPrevented, isTrue);

// Disabling again has no effect.
contextMenu.disable();
event = createDomEvent('Event', 'contextmenu');
expect(event.defaultPrevented, isFalse);
rootViewElement.dispatchEvent(event);
expect(event.defaultPrevented, isTrue);
});

test('does not disable context menu outside root view element', () {
final DomElement rootViewElement = createDomElement('div');
final ContextMenu contextMenu = ContextMenu(rootViewElement);

contextMenu.disable();

// Dispatching on a DOM element outside of target's subtree has no effect.
final DomEvent event = createDomEvent('Event', 'contextmenu');
expect(event.defaultPrevented, isFalse);
domDocument.body!.dispatchEvent(event);
expect(event.defaultPrevented, isFalse);
});

test('can enable context menu after disabling', () {
final DomElement rootViewElement = createDomElement('div');
final ContextMenu contextMenu = ContextMenu(rootViewElement);

// When the app starts, contextmenu events are not prevented.
DomEvent event = createDomEvent('Event', 'contextmenu');
expect(event.defaultPrevented, isFalse);
rootViewElement.dispatchEvent(event);
expect(event.defaultPrevented, isFalse);

// Disabling the context menu causes contextmenu events to be prevented.
contextMenu.disable();
event = createDomEvent('Event', 'contextmenu');
expect(event.defaultPrevented, isFalse);
rootViewElement.dispatchEvent(event);
expect(event.defaultPrevented, isTrue);

// Enabling the context menu means that contextmenu events are back to not
// being prevented.
contextMenu.enable();
event = createDomEvent('Event', 'contextmenu');
expect(event.defaultPrevented, isFalse);
rootViewElement.dispatchEvent(event);
expect(event.defaultPrevented, isFalse);

// Enabling again has no effect.
contextMenu.enable();
event = createDomEvent('Event', 'contextmenu');
expect(event.defaultPrevented, isFalse);
rootViewElement.dispatchEvent(event);
expect(event.defaultPrevented, isFalse);
});

test('enabling before disabling has no effect', () {
final DomElement rootViewElement = createDomElement('div');
final ContextMenu contextMenu = ContextMenu(rootViewElement);

// When the app starts, contextmenu events are not prevented.
DomEvent event = createDomEvent('Event', 'contextmenu');
expect(event.defaultPrevented, isFalse);
rootViewElement.dispatchEvent(event);
expect(event.defaultPrevented, isFalse);

// Enabling has no effect.
contextMenu.enable();
event = createDomEvent('Event', 'contextmenu');
expect(event.defaultPrevented, isFalse);
rootViewElement.dispatchEvent(event);
expect(event.defaultPrevented, isFalse);
});
});
}
Loading