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
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),
),
)
),
),
),
......
......@@ -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) {
......
......@@ -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,
),
),
),
),
......
......@@ -78,6 +78,78 @@ void main() {
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 {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
await tester.pumpWidget(
......@@ -324,5 +396,3 @@ void main() {
semantics.dispose();
});
}
......@@ -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: <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 {
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: <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 {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => FirstWidget(),
......
......@@ -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<TransformLayer>(), 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<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 {
......@@ -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 {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
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