Skip to content

How to disable browser context menu for specific widget in Flutter Web

Published: at 03:37 PM

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.


Next Post
My path to learn Vim: from Zero to Hero.