diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a8c062..de32d3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.8 + +- Improve the `ShadContextMenu` right click behavior on Web. + ## 0.9.7 - Remove kind event from `ShadMouseArea` diff --git a/lib/shadcn_ui.dart b/lib/shadcn_ui.dart index f681d6a..a4c69fe 100644 --- a/lib/shadcn_ui.dart +++ b/lib/shadcn_ui.dart @@ -97,7 +97,6 @@ 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 aaf89a3..be8877e 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,10 +1,7 @@ // ignore_for_file: lines_longer_than_80_chars 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 GlobalCupertinoLocalizations, @@ -557,15 +554,6 @@ class _ShadAppState extends State { yield GlobalWidgetsLocalizations.delegate; } - @override - void initState() { - super.initState(); - if (kIsWeb) { - // needed for disabling the native context menu on web - SemanticsBinding.instance.ensureSemantics(); - } - } - @override void dispose() { heroController.dispose(); diff --git a/lib/src/components/context_menu.dart b/lib/src/components/context_menu.dart index c042256..0a961fd 100644 --- a/lib/src/components/context_menu.dart +++ b/lib/src/components/context_menu.dart @@ -1,11 +1,17 @@ +import 'dart:async'; import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/services.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/components/button.dart'; +import 'package:shadcn_ui/src/components/popover.dart'; +import 'package:shadcn_ui/src/raw_components/portal.dart'; +import 'package:shadcn_ui/src/theme/components/decorator.dart'; +import 'package:shadcn_ui/src/theme/theme.dart'; +import 'package:shadcn_ui/src/utils/gesture_detector.dart'; +import 'package:shadcn_ui/src/utils/mouse_area.dart'; import 'package:shadcn_ui/src/utils/provider.dart'; const kContextMenuGroupId = ValueKey('context-menu'); @@ -83,7 +89,6 @@ class ShadContextMenuRegion extends StatefulWidget { } class _ShadContextMenuRegionState extends State { - final identifier = UniqueKey(); ShadContextMenuController? _controller; ShadContextMenuController get controller => widget.controller ?? @@ -91,6 +96,8 @@ class _ShadContextMenuRegionState extends State { ShadContextMenuController(isOpen: widget.visible ?? false)); Offset? offset; + final isContextMenuAlreadyDisabled = kIsWeb && !BrowserContextMenu.enabled; + @override void didUpdateWidget(covariant ShadContextMenuRegion oldWidget) { super.didUpdateWidget(oldWidget); @@ -106,6 +113,7 @@ class _ShadContextMenuRegionState extends State { } void showAtOffset(Offset offset) { + if (!mounted) return; setState(() => this.offset = offset); controller.show(); } @@ -114,8 +122,8 @@ class _ShadContextMenuRegionState extends State { controller.hide(); } - void show(TapDownDetails details) { - showAtOffset(details.globalPosition); + void show(Offset offset) { + showAtOffset(offset); } void onLongPress() { @@ -125,9 +133,11 @@ class _ShadContextMenuRegionState extends State { @override Widget build(BuildContext context) { + final platform = Theme.of(context).platform; final effectiveLongPressEnabled = widget.longPressEnabled ?? - (defaultTargetPlatform == TargetPlatform.android || - defaultTargetPlatform == TargetPlatform.iOS); + (platform == TargetPlatform.android || platform == TargetPlatform.iOS); + + final isWindows = platform == TargetPlatform.windows; return ShadContextMenu( anchor: offset == null ? null : ShadGlobalAnchor(offset!), @@ -143,7 +153,21 @@ class _ShadContextMenuRegionState extends State { filter: widget.filter, child: ShadGestureDetector( onTapDown: (_) => hide(), - onSecondaryTapDown: show, + onSecondaryTapDown: (d) async { + if (kIsWeb && !isContextMenuAlreadyDisabled) { + await BrowserContextMenu.disableContextMenu(); + } + if (!isWindows) show(d.globalPosition); + }, + onSecondaryTapUp: (d) async { + if (isWindows) { + show(d.globalPosition); + await Future.delayed(Duration.zero); + } + if (kIsWeb && !isContextMenuAlreadyDisabled) { + await BrowserContextMenu.enableContextMenu(); + } + }, onLongPressStart: effectiveLongPressEnabled ? (d) { offset = d.globalPosition; @@ -297,6 +321,7 @@ class ShadContextMenuState extends State { effects: effectiveEffects, shadows: effectiveShadows, filter: effectiveFilter, + useSameGroupIdForChild: false, popover: (context) { return ShadMouseArea( groupId: widget.groupId, @@ -319,13 +344,11 @@ class ShadContextMenuState extends State { ), ); }, - child: DisableWebContextMenu( - child: ShadMouseArea( - groupId: widget.groupId, - onEnter: (_) => widget.onHoverArea?.call(true), - onExit: (_) => widget.onHoverArea?.call(false), - child: widget.child, - ), + child: ShadMouseArea( + groupId: widget.groupId, + onEnter: (_) => widget.onHoverArea?.call(true), + onExit: (_) => widget.onHoverArea?.call(false), + child: widget.child, ), ); diff --git a/lib/src/components/popover.dart b/lib/src/components/popover.dart index 468fda6..5795094 100644 --- a/lib/src/components/popover.dart +++ b/lib/src/components/popover.dart @@ -59,6 +59,7 @@ class ShadPopover extends StatefulWidget { this.filter, this.groupId, this.areaGroupId, + this.useSameGroupIdForChild = true, }) : assert( (controller != null) ^ (visible != null), 'Either controller or visible must be provided', @@ -132,6 +133,13 @@ class ShadPopover extends StatefulWidget { /// {@macro ShadMouseArea.groupId} final Object? areaGroupId; + /// {@template ShadPopover.useSameGroupIdForChild} + /// Whether the [groupId] should be used for the child widget, defaults to + /// `true`. This teams that taps on the child widget will be handled as inside + /// the popover. + /// {@endtemplate} + final bool useSameGroupIdForChild; + @override State createState() => _ShadPopoverState(); } @@ -239,26 +247,30 @@ class _ShadPopoverState extends State { ); } - return TapRegion( - groupId: groupId, - child: ListenableBuilder( - listenable: controller, - builder: (context, _) { - return CallbackShortcuts( - bindings: { - const SingleActivator(LogicalKeyboardKey.escape): () { - controller.hide(); - }, + Widget child = ListenableBuilder( + listenable: controller, + builder: (context, _) { + return CallbackShortcuts( + bindings: { + const SingleActivator(LogicalKeyboardKey.escape): () { + controller.hide(); }, - child: ShadPortal( - portalBuilder: (_) => popover, - visible: controller.isOpen, - anchor: effectiveAnchor, - child: widget.child, - ), - ); - }, - ), + }, + child: ShadPortal( + portalBuilder: (_) => popover, + visible: controller.isOpen, + anchor: effectiveAnchor, + child: widget.child, + ), + ); + }, ); + if (widget.useSameGroupIdForChild) { + child = TapRegion( + groupId: groupId, + child: child, + ); + } + return 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 deleted file mode 100644 index 6d0fc6a..0000000 --- a/lib/src/utils/disable_context_menu/disable_context_menu.dart +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index a54860c..0000000 --- a/lib/src/utils/disable_context_menu/non_web.dart +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 7c985c1..0000000 --- a/lib/src/utils/disable_context_menu/web.dart +++ /dev/null @@ -1,90 +0,0 @@ -// 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(); - break; - } - } - } - } - } - }); - - 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 beb37f3..4d54465 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.7 +version: 0.9.8 homepage: https://mariuti.com/shadcn-ui repository: https://github.com/nank1ro/flutter-shadcn-ui documentation: https://mariuti.com/shadcn-ui