Unverified Commit 01f4f1ac authored by Tong Mu's avatar Tong Mu Committed by GitHub

ModalBarrier and Drawer barrier prevents mouse events (#44296)

* Add opaque to barriers
* Detect opaque and test
parent 2d42b43a
...@@ -546,9 +546,12 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro ...@@ -546,9 +546,12 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
onTap: close, onTap: close,
child: Semantics( child: Semantics(
label: MaterialLocalizations.of(context)?.modalBarrierDismissLabel, label: MaterialLocalizations.of(context)?.modalBarrierDismissLabel,
child: Container( // The drawer's "scrim" child: MouseRegion(
color: _scrimColorTween.evaluate(_controller), opaque: true,
), child: Container( // The drawer's "scrim"
color: _scrimColorTween.evaluate(_controller),
),
)
), ),
), ),
), ),
......
...@@ -2615,12 +2615,13 @@ class RenderMouseRegion extends RenderProxyBox { ...@@ -2615,12 +2615,13 @@ class RenderMouseRegion extends RenderProxyBox {
PointerEnterEventListener onEnter, PointerEnterEventListener onEnter,
PointerHoverEventListener onHover, PointerHoverEventListener onHover,
PointerExitEventListener onExit, PointerExitEventListener onExit,
this.opaque = true, bool opaque = true,
RenderBox child, RenderBox child,
}) : assert(opaque != null), }) : assert(opaque != null),
_onEnter = onEnter, _onEnter = onEnter,
_onHover = onHover, _onHover = onHover,
_onExit = onExit, _onExit = onExit,
_opaque = opaque,
_annotationIsActive = false, _annotationIsActive = false,
super(child) { super(child) {
_hoverAnnotation = MouseTrackerAnnotation( _hoverAnnotation = MouseTrackerAnnotation(
...@@ -2644,7 +2645,14 @@ class RenderMouseRegion extends RenderProxyBox { ...@@ -2644,7 +2645,14 @@ class RenderMouseRegion extends RenderProxyBox {
/// pointer is within their areas. /// pointer is within their areas.
/// ///
/// This defaults to true. /// 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 /// Called when a mouse pointer enters the region (with or without buttons
/// pressed). /// pressed).
...@@ -2705,7 +2713,8 @@ class RenderMouseRegion extends RenderProxyBox { ...@@ -2705,7 +2713,8 @@ class RenderMouseRegion extends RenderProxyBox {
final bool annotationWillBeActive = ( final bool annotationWillBeActive = (
_onEnter != null || _onEnter != null ||
_onHover != null || _onHover != null ||
_onExit != null _onExit != null ||
opaque
) && ) &&
RendererBinding.instance.mouseTracker.mouseIsConnected; RendererBinding.instance.mouseTracker.mouseIsConnected;
if (annotationWasActive != annotationWillBeActive) { if (annotationWasActive != annotationWillBeActive) {
......
...@@ -101,11 +101,14 @@ class ModalBarrier extends StatelessWidget { ...@@ -101,11 +101,14 @@ class ModalBarrier extends StatelessWidget {
child: Semantics( child: Semantics(
label: semanticsDismissible ? semanticsLabel : null, label: semanticsDismissible ? semanticsLabel : null,
textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null, textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null,
child: ConstrainedBox( child: MouseRegion(
constraints: const BoxConstraints.expand(), opaque: true,
child: color == null ? null : DecoratedBox( child: ConstrainedBox(
decoration: BoxDecoration( constraints: const BoxConstraints.expand(),
color: color, child: color == null ? null : DecoratedBox(
decoration: BoxDecoration(
color: color,
),
), ),
), ),
), ),
......
...@@ -78,6 +78,78 @@ void main() { ...@@ -78,6 +78,78 @@ void main() {
expect(find.text('drawer'), findsNothing); expect(find.text('drawer'), findsNothing);
}); });
testWidgets('Drawer hover test', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final List<String> logs = <String>[];
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, <String>['enter', 'hover']);
logs.clear();
await gesture.moveTo(const Offset(20, 20));
await tester.pump(); // no effect
expect(logs, <String>['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, <String>['enter', 'hover']);
logs.clear();
await gesture.moveTo(const Offset(20, 20));
await tester.pump(); // no effect
expect(logs, <String>['exit']);
logs.clear();
});
testWidgets('Drawer drag cancel resume (LTR)', (WidgetTester tester) async { testWidgets('Drawer drag cancel resume (LTR)', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
await tester.pumpWidget( await tester.pumpWidget(
...@@ -324,5 +396,3 @@ void main() { ...@@ -324,5 +396,3 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
} }
...@@ -7,13 +7,15 @@ import 'package:flutter/foundation.dart'; ...@@ -7,13 +7,15 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.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'; import 'semantics_tester.dart';
void main() { void main() {
bool tapped; bool tapped;
bool hovered;
Widget tapTarget; Widget tapTarget;
Widget hoverTarget;
setUp(() { setUp(() {
tapped = false; tapped = false;
...@@ -27,6 +29,18 @@ void main() { ...@@ -27,6 +29,18 @@ void main() {
child: Text('target', textDirection: TextDirection.ltr), 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 { testWidgets('ModalBarrier prevents interactions with widgets behind it', (WidgetTester tester) async {
...@@ -45,6 +59,35 @@ void main() { ...@@ -45,6 +59,35 @@ void main() {
reason: 'because the tap is not prevented by ModalBarrier'); 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: <Widget>[
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 { testWidgets('ModalBarrier does not prevent interactions with widgets in front of it', (WidgetTester tester) async {
final Widget subject = Stack( final Widget subject = Stack(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
...@@ -89,6 +132,37 @@ void main() { ...@@ -89,6 +132,37 @@ void main() {
reason: 'because the drag is prevented by ModalBarrier'); 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: <Widget>[
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 { testWidgets('ModalBarrier pops the Navigator when dismissed by primay tap', (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => FirstWidget(), '/': (BuildContext context) => FirstWidget(),
......
...@@ -523,7 +523,7 @@ void main() { ...@@ -523,7 +523,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
Transform.scale( Transform.scale(
scale: 2.0, scale: 2.0,
child: const MouseRegion(), child: const MouseRegion(opaque: false),
), ),
); );
final RenderMouseRegion listener = tester.renderObject(find.byType(MouseRegion)); final RenderMouseRegion listener = tester.renderObject(find.byType(MouseRegion));
...@@ -534,10 +534,12 @@ void main() { ...@@ -534,10 +534,12 @@ void main() {
// transform.) // transform.)
expect(tester.layers.whereType<TransformLayer>(), hasLength(1)); expect(tester.layers.whereType<TransformLayer>(), hasLength(1));
// Test that needsCompositing updates correctly with callback change
await tester.pumpWidget( await tester.pumpWidget(
Transform.scale( Transform.scale(
scale: 2.0, scale: 2.0,
child: MouseRegion( child: MouseRegion(
opaque: false,
onHover: (PointerHoverEvent _) {}, onHover: (PointerHoverEvent _) {},
), ),
), ),
...@@ -550,13 +552,27 @@ void main() { ...@@ -550,13 +552,27 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
Transform.scale( Transform.scale(
scale: 2.0, scale: 2.0,
child: const MouseRegion(), child: const MouseRegion(opaque: false),
), ),
); );
expect(listener.needsCompositing, isFalse); expect(listener.needsCompositing, isFalse);
// TransformLayer for `Transform.scale` is removed again as transform is // TransformLayer for `Transform.scale` is removed again as transform is
// executed directly on the canvas. // executed directly on the canvas.
expect(tester.layers.whereType<TransformLayer>(), hasLength(1)); expect(tester.layers.whereType<TransformLayer>(), 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<TransformLayer>(), hasLength(2));
}); });
testWidgets("Callbacks aren't called during build", (WidgetTester tester) async { testWidgets("Callbacks aren't called during build", (WidgetTester tester) async {
...@@ -942,6 +958,42 @@ void main() { ...@@ -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: <Widget>[
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 { testWidgets('RenderMouseRegion\'s debugFillProperties when default', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
RenderMouseRegion().debugFillProperties(builder); RenderMouseRegion().debugFillProperties(builder);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment