In HTML you can disable the default browser context menu for a specific element setting the oncontextmenu
attribute to return false;
, example:
<div oncontextmenu="return false;"></div>
But in Flutter Web, there is no 1:1 binding between the HTML and a Flutter widget, especially when you use the canvaskit web renderer.
In this blog post I’m going to show you how to disable the browser context menu for a specific widget in Flutter Web.
This code works for each flutter web renderer, it’s not dependent on the renderer, so you can use it with the html renderer too.
Semantics
To assign an identifier to a widget on web, you can use the Semantics
widget with the identifier
property.
Semantics(
identitier: 'my-widget',
child: MyWidget(),
),
This, on Flutter Web, will assign an attribute called flt-semantics-identifier
to the corresponding HTML slot where the canvas is rendered.
But if you try to run the app, nothing will happen. No flt-semantics-identifier
attribute will be added to any HTML.
But the feature has been merged. How come it doesn’t work?
This particular issue talks about assigning an identifier to a widget on Flutter Web.
While this is the pull request that implemented it.
After some time, I think about two weeks, while reading this article on Medium I discovered that it is a feature that you have to opt-in for, by default it’s disabled.
To enable it, simply execute this code:
if (kIsWeb) {
SemanticsBinding.instance.ensureSemantics();
}
Great! Now you can find the flt-semantics-identifier
attribute in the HTML element.
Context menu
My library flutter_shadcn_ui has a widget called ContextMenu
.
Until I discovered this possibility with the Semantics
widget, I had been using this code:
if (kIsWeb) {
BrowserContextMenu.disableContextMenu();
}
The problem with this code is that it disables the context menu for the whole app, i.e. for all widgets.
For this reason, I decided to create a widget called DisableWebContextMenu
that disables the context menu for its child
DisableWebContextMenu
flutter_shadcn_ui works on all platforms, so we need to make two different versions of the widget. One for the web and one for the other platforms. Here it is the implementation for non-web platforms:
import 'package:flutter/widgets.dart';
class DisableWebContextMenu extends StatelessWidget {
const DisableWebContextMenu({
super.key,
required this.child,
this.identifier,
});
final String? identifier;
final Widget child;
@override
Widget build(BuildContext context) {
// no-op on non-web platforms
return child;
}
}
In non-web platforms, the widget above does nothing, just returns the child
widget.
While on web the implementation is the following:
import 'dart:html' as html;
import 'package:flutter/widgets.dart';
class DisableWebContextMenu extends StatefulWidget {
const DisableWebContextMenu({
super.key,
required this.child,
this.identifier,
});
final String? identifier;
final Widget child;
@override
State<DisableWebContextMenu> createState() => _DisableWebContextMenuState();
}
class _DisableWebContextMenuState extends State<DisableWebContextMenu> {
// The observer used to listen for changes in the DOM
html.MutationObserver? observer;
// Internal unique identifier, used if no custom identifier is provided
final _identifier = UniqueKey();
// The identifier to use, either the one provided or the internal one
String get identifier => widget.identifier ?? _identifier.toString();
@override
void initState() {
super.initState();
// After the build method has finished
WidgetsBinding.instance.addPostFrameCallback((_) {
// Find the element with the identifier
final element = findElement();
if (element != null) {
// If the element is found, disable the context menu
element.setAttribute('oncontextmenu', 'return false;');
} else {
// If the element is not found, add an observer
addObserver();
}
});
}
// Finds the element with the identifier
html.Element? findElement() => html.document
.querySelector('flt-semantics-host')
?.querySelector('[flt-semantics-identifier="$identifier"]');
// Listens for changes in the DOM, until the element is found
void addObserver() {
observer = html.MutationObserver((mutations, _) {
for (final mutation in mutations) {
if (mutation is! html.MutationRecord) continue;
if (mutation.addedNodes?.isNotEmpty ?? false) {
for (final node in mutation.addedNodes!) {
if (node is html.HtmlElement) {
final id = node.attributes['flt-semantics-identifier'];
// if the identifier matches, disable the context menu
// and remove the observer
if (id == identifier) {
node.setAttribute('oncontextmenu', 'return false;');
removeObserver();
break;
}
}
}
}
}
});
// Add the observer to the whole document
observer!.observe(
html.document,
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['flt-semantics-identifier'],
);
}
// Removes the observer
void removeObserver() {
observer?.disconnect();
}
@override
void dispose() {
// Remove the observer when the widget is disposed
removeObserver();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Assigns an identifier to the child widget
return Semantics(
identifier: identifier,
child: widget.child,
);
}
}
All this code will do the magic.
Now to finish we need a barrel file to export the two implementations:
export 'non_web.dart' if (dart.library.js_interop) 'web.dart';
I’m not using dart.library.html
for a reason, see this comment on X.
Said quickly, the dart.library.html
is not available on Flutter Web when using WASM.
Conclusion
I hope this post was useful for you. This fix was a bit tricky, but it works.
This is the commit that implemented the fix on Flutter Shadcn UI which has been released in the version 0.9.6.