diff --git a/packages/flutter/lib/src/material/drawer.dart b/packages/flutter/lib/src/material/drawer.dart index 4287a18ab8..a1c4b0d008 100644 --- a/packages/flutter/lib/src/material/drawer.dart +++ b/packages/flutter/lib/src/material/drawer.dart @@ -546,9 +546,12 @@ class DrawerControllerState extends State with SingleTickerPro onTap: close, child: Semantics( label: MaterialLocalizations.of(context)?.modalBarrierDismissLabel, - child: Container( // The drawer's "scrim" - color: _scrimColorTween.evaluate(_controller), - ), + child: MouseRegion( + opaque: true, + child: Container( // The drawer's "scrim" + color: _scrimColorTween.evaluate(_controller), + ), + ) ), ), ), diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 5777f1c6c7..33021aeacd 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -2615,12 +2615,13 @@ class RenderMouseRegion extends RenderProxyBox { PointerEnterEventListener onEnter, PointerHoverEventListener onHover, PointerExitEventListener onExit, - this.opaque = true, + bool opaque = true, RenderBox child, }) : assert(opaque != null), _onEnter = onEnter, _onHover = onHover, _onExit = onExit, + _opaque = opaque, _annotationIsActive = false, super(child) { _hoverAnnotation = MouseTrackerAnnotation( @@ -2644,7 +2645,14 @@ class RenderMouseRegion extends RenderProxyBox { /// pointer is within their areas. /// /// This defaults to true. - bool opaque; + bool get opaque => _opaque; + bool _opaque; + set opaque(bool value) { + if (_opaque != value) { + _opaque = value; + _updateAnnotations(); + } + } /// Called when a mouse pointer enters the region (with or without buttons /// pressed). @@ -2705,7 +2713,8 @@ class RenderMouseRegion extends RenderProxyBox { final bool annotationWillBeActive = ( _onEnter != null || _onHover != null || - _onExit != null + _onExit != null || + opaque ) && RendererBinding.instance.mouseTracker.mouseIsConnected; if (annotationWasActive != annotationWillBeActive) { diff --git a/packages/flutter/lib/src/widgets/modal_barrier.dart b/packages/flutter/lib/src/widgets/modal_barrier.dart index af633cce5c..14de51e77d 100644 --- a/packages/flutter/lib/src/widgets/modal_barrier.dart +++ b/packages/flutter/lib/src/widgets/modal_barrier.dart @@ -101,11 +101,14 @@ class ModalBarrier extends StatelessWidget { child: Semantics( label: semanticsDismissible ? semanticsLabel : null, textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null, - child: ConstrainedBox( - constraints: const BoxConstraints.expand(), - child: color == null ? null : DecoratedBox( - decoration: BoxDecoration( - color: color, + child: MouseRegion( + opaque: true, + child: ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: color == null ? null : DecoratedBox( + decoration: BoxDecoration( + color: color, + ), ), ), ), diff --git a/packages/flutter/test/widgets/drawer_test.dart b/packages/flutter/test/widgets/drawer_test.dart index 5dc9a7f6f2..ffa5594b59 100644 --- a/packages/flutter/test/widgets/drawer_test.dart +++ b/packages/flutter/test/widgets/drawer_test.dart @@ -78,6 +78,78 @@ void main() { expect(find.text('drawer'), findsNothing); }); + testWidgets('Drawer hover test', (WidgetTester tester) async { + final GlobalKey scaffoldKey = GlobalKey(); + final List logs = []; + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + addTearDown(gesture.removePointer); + + // Start out of hoverTarget + await gesture.moveTo(const Offset(100, 100)); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + key: scaffoldKey, + drawer: const Text('drawer'), + body: Align( + alignment: Alignment.topLeft, + child: MouseRegion( + onEnter: (_) { logs.add('enter'); }, + onHover: (_) { logs.add('hover'); }, + onExit: (_) { logs.add('exit'); }, + child: Container(width: 10, height: 10), + ), + ), + ), + ), + ); + expect(logs, isEmpty); + expect(find.text('drawer'), findsNothing); + + // When drawer is closed, hover is interactable + await gesture.moveTo(const Offset(5, 5)); + await tester.pump(); // no effect + expect(logs, ['enter', 'hover']); + logs.clear(); + + await gesture.moveTo(const Offset(20, 20)); + await tester.pump(); // no effect + expect(logs, ['exit']); + logs.clear(); + + // When drawer is open, hover is uninteractable + scaffoldKey.currentState.openDrawer(); + await tester.pump(const Duration(seconds: 1)); // animation done + expect(find.text('drawer'), findsOneWidget); + + await gesture.moveTo(const Offset(5, 5)); + await tester.pump(); // no effect + expect(logs, isEmpty); + logs.clear(); + + await gesture.moveTo(const Offset(20, 20)); + await tester.pump(); // no effect + expect(logs, isEmpty); + logs.clear(); + + // Close drawer, hover is interactable again + await tester.tapAt(const Offset(750.0, 100.0)); // on the mask + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); // animation done + expect(find.text('drawer'), findsNothing); + + await gesture.moveTo(const Offset(5, 5)); + await tester.pump(); // no effect + expect(logs, ['enter', 'hover']); + logs.clear(); + + await gesture.moveTo(const Offset(20, 20)); + await tester.pump(); // no effect + expect(logs, ['exit']); + logs.clear(); + }); + testWidgets('Drawer drag cancel resume (LTR)', (WidgetTester tester) async { final GlobalKey scaffoldKey = GlobalKey(); await tester.pumpWidget( @@ -324,5 +396,3 @@ void main() { semantics.dispose(); }); } - - diff --git a/packages/flutter/test/widgets/modal_barrier_test.dart b/packages/flutter/test/widgets/modal_barrier_test.dart index 6fd27d8a0f..f1314ef5f3 100644 --- a/packages/flutter/test/widgets/modal_barrier_test.dart +++ b/packages/flutter/test/widgets/modal_barrier_test.dart @@ -7,13 +7,15 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter/gestures.dart' show kSecondaryButton; +import 'package:flutter/gestures.dart' show kSecondaryButton, PointerDeviceKind; import 'semantics_tester.dart'; void main() { bool tapped; + bool hovered; Widget tapTarget; + Widget hoverTarget; setUp(() { tapped = false; @@ -27,6 +29,18 @@ void main() { child: Text('target', textDirection: TextDirection.ltr), ), ); + + hovered = false; + hoverTarget = MouseRegion( + onHover: (_) { hovered = true; }, + onEnter: (_) { hovered = true; }, + onExit: (_) { hovered = true; }, + child: const SizedBox( + width: 10.0, + height: 10.0, + child: Text('target', textDirection: TextDirection.ltr), + ), + ); }); testWidgets('ModalBarrier prevents interactions with widgets behind it', (WidgetTester tester) async { @@ -45,6 +59,35 @@ void main() { reason: 'because the tap is not prevented by ModalBarrier'); }); + testWidgets('ModalBarrier prevents hover interactions with widgets behind it', (WidgetTester tester) async { + final Widget subject = Stack( + textDirection: TextDirection.ltr, + children: [ + hoverTarget, + const ModalBarrier(dismissible: false), + ], + ); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + addTearDown(gesture.removePointer); + // Start out of hoverTarget + await gesture.moveTo(const Offset(100, 100)); + + await tester.pumpWidget(subject); + // Move into hoverTarget and tap + await gesture.down(const Offset(5, 5)); + await tester.pumpWidget(subject); + await gesture.up(); + await tester.pumpWidget(subject); + + // Move out + await gesture.moveTo(const Offset(100, 100)); + await tester.pumpWidget(subject); + + expect(hovered, isFalse, + reason: 'because the hover is not prevented by ModalBarrier'); + }); + testWidgets('ModalBarrier does not prevent interactions with widgets in front of it', (WidgetTester tester) async { final Widget subject = Stack( textDirection: TextDirection.ltr, @@ -89,6 +132,37 @@ void main() { reason: 'because the drag is prevented by ModalBarrier'); }); + testWidgets('ModalBarrier does not prevent hover interactions with widgets in front of it', (WidgetTester tester) async { + final Widget subject = Stack( + textDirection: TextDirection.ltr, + children: [ + const ModalBarrier(dismissible: false), + hoverTarget, + ], + ); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + addTearDown(gesture.removePointer); + // Start out of hoverTarget + await gesture.moveTo(const Offset(100, 100)); + await tester.pumpWidget(subject); + expect(hovered, isFalse); + + // Move into hoverTarget + await gesture.moveTo(const Offset(5, 5)); + await tester.pumpWidget(subject); + expect(hovered, isTrue, + reason: 'because the hover is prevented by ModalBarrier'); + hovered = false; + + // Move out + await gesture.moveTo(const Offset(100, 100)); + await tester.pumpWidget(subject); + expect(hovered, isTrue, + reason: 'because the hover is prevented by ModalBarrier'); + hovered = false; + }); + testWidgets('ModalBarrier pops the Navigator when dismissed by primay tap', (WidgetTester tester) async { final Map routes = { '/': (BuildContext context) => FirstWidget(), diff --git a/packages/flutter/test/widgets/mouse_region_test.dart b/packages/flutter/test/widgets/mouse_region_test.dart index d959175f9a..22984dd01b 100644 --- a/packages/flutter/test/widgets/mouse_region_test.dart +++ b/packages/flutter/test/widgets/mouse_region_test.dart @@ -523,7 +523,7 @@ void main() { await tester.pumpWidget( Transform.scale( scale: 2.0, - child: const MouseRegion(), + child: const MouseRegion(opaque: false), ), ); final RenderMouseRegion listener = tester.renderObject(find.byType(MouseRegion)); @@ -534,10 +534,12 @@ void main() { // transform.) expect(tester.layers.whereType(), hasLength(1)); + // Test that needsCompositing updates correctly with callback change await tester.pumpWidget( Transform.scale( scale: 2.0, child: MouseRegion( + opaque: false, onHover: (PointerHoverEvent _) {}, ), ), @@ -550,13 +552,27 @@ void main() { await tester.pumpWidget( Transform.scale( scale: 2.0, - child: const MouseRegion(), + child: const MouseRegion(opaque: false), ), ); expect(listener.needsCompositing, isFalse); // TransformLayer for `Transform.scale` is removed again as transform is // executed directly on the canvas. expect(tester.layers.whereType(), hasLength(1)); + + // Test that needsCompositing updates correctly with `opaque` change + await tester.pumpWidget( + Transform.scale( + scale: 2.0, + child: const MouseRegion( + opaque: true, + ), + ), + ); + expect(listener.needsCompositing, isTrue); + // Compositing is required, therefore a dedicated TransformLayer for + // `Transform.scale` is added. + expect(tester.layers.whereType(), hasLength(2)); }); testWidgets("Callbacks aren't called during build", (WidgetTester tester) async { @@ -942,6 +958,42 @@ void main() { }); }); + testWidgets('an empty opaque MouseRegion is effective', (WidgetTester tester) async { + bool bottomRegionIsHovered = false; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Stack( + children: [ + Align( + alignment: Alignment.topLeft, + child: MouseRegion( + onEnter: (_) { bottomRegionIsHovered = true; }, + onHover: (_) { bottomRegionIsHovered = true; }, + onExit: (_) { bottomRegionIsHovered = true; }, + child: Container( + width: 10, + height: 10, + ), + ), + ), + const MouseRegion(opaque: true), + ], + ), + ), + ); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: const Offset(20, 20)); + addTearDown(gesture.removePointer); + + await gesture.moveTo(const Offset(5, 5)); + await tester.pump(); + await gesture.moveTo(const Offset(20, 20)); + await tester.pump(); + expect(bottomRegionIsHovered, isFalse); + }); + testWidgets('RenderMouseRegion\'s debugFillProperties when default', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); RenderMouseRegion().debugFillProperties(builder);