From 87fbdd904ba105949797e314dbcb5bd76c0cc559 Mon Sep 17 00:00:00 2001 From: Alexandru Mariuti Date: Fri, 20 Sep 2024 15:12:22 +0100 Subject: [PATCH] fix/disable-context-menu --- CHANGELOG.md | 4 + lib/shadcn_ui.dart | 1 + lib/src/app.dart | 5 +- lib/src/components/context_menu.dart | 32 ++++--- .../disable_context_menu.dart | 1 + .../utils/disable_context_menu/non_web.dart | 18 ++++ lib/src/utils/disable_context_menu/web.dart | 89 +++++++++++++++++++ pubspec.yaml | 2 +- 8 files changed, 135 insertions(+), 17 deletions(-) create mode 100644 lib/src/utils/disable_context_menu/disable_context_menu.dart create mode 100644 lib/src/utils/disable_context_menu/non_web.dart create mode 100644 lib/src/utils/disable_context_menu/web.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 3be2aa59..237b9d28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.6 + +- Fix: the browser context menu has been enabled again, and deactivated only for the `ShadContextMenu` component. + ## 0.9.5 - Add text selection toolbar to `ShadInput` (thanks to @moshOntong-IT). diff --git a/lib/shadcn_ui.dart b/lib/shadcn_ui.dart index a4c69fef..f681d6a3 100644 --- a/lib/shadcn_ui.dart +++ b/lib/shadcn_ui.dart @@ -97,6 +97,7 @@ export 'src/utils/provider.dart' hide ProviderReadExt, ProviderWatchExt; export 'src/utils/responsive.dart'; export 'src/utils/states_controller.dart'; export 'src/utils/mouse_area.dart'; +export 'src/utils/disable_context_menu/disable_context_menu.dart'; // External libraries export 'package:flutter_animate/flutter_animate.dart' hide Effect; diff --git a/lib/src/app.dart b/lib/src/app.dart index d07472da..aaf89a3e 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart' show @@ -559,9 +560,9 @@ class _ShadAppState extends State { @override void initState() { super.initState(); - // This could be centralized in the context menu component, see https://github.com/flutter/engine/pull/53278#issuecomment-2328309843 if (kIsWeb) { - BrowserContextMenu.disableContextMenu(); + // needed for disabling the native context menu on web + SemanticsBinding.instance.ensureSemantics(); } } diff --git a/lib/src/components/context_menu.dart b/lib/src/components/context_menu.dart index e8a8e37e..c042256e 100644 --- a/lib/src/components/context_menu.dart +++ b/lib/src/components/context_menu.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; +import 'package:shadcn_ui/src/utils/disable_context_menu/disable_context_menu.dart'; import 'package:shadcn_ui/src/utils/provider.dart'; const kContextMenuGroupId = ValueKey('context-menu'); @@ -82,6 +83,7 @@ class ShadContextMenuRegion extends StatefulWidget { } class _ShadContextMenuRegionState extends State { + final identifier = UniqueKey(); ShadContextMenuController? _controller; ShadContextMenuController get controller => widget.controller ?? @@ -130,6 +132,15 @@ class _ShadContextMenuRegionState extends State { return ShadContextMenu( anchor: offset == null ? null : ShadGlobalAnchor(offset!), controller: controller, + children: widget.children, + constraints: widget.constraints, + onHoverArea: widget.onHoverArea, + padding: widget.padding, + groupId: widget.groupId, + effects: widget.effects, + shadows: widget.shadows, + decoration: widget.decoration, + filter: widget.filter, child: ShadGestureDetector( onTapDown: (_) => hide(), onSecondaryTapDown: show, @@ -141,15 +152,6 @@ class _ShadContextMenuRegionState extends State { onLongPress: effectiveLongPressEnabled ? onLongPress : null, child: widget.child, ), - children: widget.children, - constraints: widget.constraints, - onHoverArea: widget.onHoverArea, - padding: widget.padding, - groupId: widget.groupId, - effects: widget.effects, - shadows: widget.shadows, - decoration: widget.decoration, - filter: widget.filter, ); } } @@ -317,11 +319,13 @@ class ShadContextMenuState extends State { ), ); }, - child: ShadMouseArea( - groupId: widget.groupId, - onEnter: (_) => widget.onHoverArea?.call(true), - onExit: (_) => widget.onHoverArea?.call(false), - child: widget.child, + child: DisableWebContextMenu( + child: ShadMouseArea( + groupId: widget.groupId, + onEnter: (_) => widget.onHoverArea?.call(true), + onExit: (_) => widget.onHoverArea?.call(false), + child: widget.child, + ), ), ); diff --git a/lib/src/utils/disable_context_menu/disable_context_menu.dart b/lib/src/utils/disable_context_menu/disable_context_menu.dart new file mode 100644 index 00000000..6d0fc6a9 --- /dev/null +++ b/lib/src/utils/disable_context_menu/disable_context_menu.dart @@ -0,0 +1 @@ +export 'non_web.dart' if (dart.library.js_interop) 'web.dart'; diff --git a/lib/src/utils/disable_context_menu/non_web.dart b/lib/src/utils/disable_context_menu/non_web.dart new file mode 100644 index 00000000..a54860ca --- /dev/null +++ b/lib/src/utils/disable_context_menu/non_web.dart @@ -0,0 +1,18 @@ +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; + } +} diff --git a/lib/src/utils/disable_context_menu/web.dart b/lib/src/utils/disable_context_menu/web.dart new file mode 100644 index 00000000..759ee1c2 --- /dev/null +++ b/lib/src/utils/disable_context_menu/web.dart @@ -0,0 +1,89 @@ +// ignore_for_file: avoid_web_libraries_in_flutter + +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 createState() => _DisableWebContextMenuState(); +} + +class _DisableWebContextMenuState extends State { + html.MutationObserver? observer; + + final _identifier = UniqueKey(); + + String get identifier => widget.identifier ?? _identifier.toString(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + final element = findElement(); + if (element != null) { + element.setAttribute('oncontextmenu', 'return false;'); + } else { + addObserver(); + } + }); + } + + html.Element? findElement() => html.document + .querySelector('flt-semantics-host') + ?.querySelector('[flt-semantics-identifier="$identifier"]'); + + 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 (id == identifier) { + node.setAttribute('oncontextmenu', 'return false;'); + removeObserver(); + } + } + } + } + } + }); + + observer!.observe( + html.document, + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['flt-semantics-identifier'], + ); + } + + void removeObserver() { + observer?.disconnect(); + } + + @override + void dispose() { + removeObserver(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Semantics( + identifier: identifier, + child: widget.child, + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 93d60015..a5ddd2fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: shadcn_ui description: shadcn-ui ported in Flutter. Awesome UI components for Flutter, fully customizable. -version: 0.9.5 +version: 0.9.6 homepage: https://mariuti.com/shadcn-ui repository: https://github.com/nank1ro/flutter-shadcn-ui documentation: https://mariuti.com/shadcn-ui