Unverified Commit b6ceff55 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

re-add semantic service events for tap and longPress (#17918)

parent 7587f54b
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/rendering.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -86,6 +88,7 @@ class Feedback { ...@@ -86,6 +88,7 @@ class Feedback {
/// * [wrapForTap] to trigger platform-specific feedback before executing a /// * [wrapForTap] to trigger platform-specific feedback before executing a
/// [GestureTapCallback]. /// [GestureTapCallback].
static Future<Null> forTap(BuildContext context) async { static Future<Null> forTap(BuildContext context) async {
context.findRenderObject().sendSemanticsEvent(const TapSemanticEvent());
switch (_platform(context)) { switch (_platform(context)) {
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
...@@ -124,6 +127,7 @@ class Feedback { ...@@ -124,6 +127,7 @@ class Feedback {
/// * [wrapForLongPress] to trigger platform-specific feedback before /// * [wrapForLongPress] to trigger platform-specific feedback before
/// executing a [GestureLongPressCallback]. /// executing a [GestureLongPressCallback].
static Future<Null> forLongPress(BuildContext context) { static Future<Null> forLongPress(BuildContext context) {
context.findRenderObject().sendSemanticsEvent(const LongPressSemanticsEvent());
switch (_platform(context)) { switch (_platform(context)) {
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
......
...@@ -291,6 +291,7 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -291,6 +291,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
onChanged(false); onChanged(false);
break; break;
} }
sendSemanticsEvent(const TapSemanticEvent());
} }
void _handleTapUp(TapUpDetails details) { void _handleTapUp(TapUpDetails details) {
......
...@@ -2186,6 +2186,25 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2186,6 +2186,25 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
// Nothing to do by default. // Nothing to do by default.
} }
/// Sends a [SemanticsEvent] associated with this render object's [SemanticsNode].
///
/// If this render object has no semantics information, the first parent
/// render object with a non-null semantic node is used.
///
/// If semantics are disabled, no events are dispatched.
///
/// See [SemanticsNode.sendEvent] for a full description of the behavior.
void sendSemanticsEvent(SemanticsEvent semanticsEvent) {
if (owner.semanticsOwner == null)
return;
if (_semantics != null) {
_semantics.sendEvent(semanticsEvent);
} else if (parent != null) {
final RenderObject renderParent = parent;
renderParent.sendSemanticsEvent(semanticsEvent);
}
}
// Use [_semanticsConfiguration] to access. // Use [_semanticsConfiguration] to access.
SemanticsConfiguration _cachedSemanticsConfiguration; SemanticsConfiguration _cachedSemanticsConfiguration;
......
...@@ -107,3 +107,29 @@ class TooltipSemanticsEvent extends SemanticsEvent { ...@@ -107,3 +107,29 @@ class TooltipSemanticsEvent extends SemanticsEvent {
}; };
} }
} }
/// An event which triggers long press semantic feedback.
///
/// Currently only honored on Android. Triggers a long-press specific sound
/// when TalkBack is enabled.
class LongPressSemanticsEvent extends SemanticsEvent {
/// Constructs an event that triggers a long-press semantic feedback by the platform.
const LongPressSemanticsEvent() : super('longPress');
@override
Map<String, dynamic> getDataMap() => const <String, dynamic>{};
}
/// An event which triggers tap semantic feedback.
///
/// Currently only honored on Android. Triggers a tap specific sound when
/// TalkBack is enabled.
class TapSemanticEvent extends SemanticsEvent {
/// Constructs an event that triggers a long-press semantic feedback by the platform.
const TapSemanticEvent() : super('tap');
@override
Map<String, dynamic> getDataMap() => const <String, dynamic>{};
}
...@@ -34,7 +34,8 @@ class SemanticsService { ...@@ -34,7 +34,8 @@ class SemanticsService {
/// Sends a semantic announcement of a tooltip. /// Sends a semantic announcement of a tooltip.
/// ///
/// This is only used by Android. /// Currently only honored on Android. The contents of [message] will be
/// read by TalkBack.
static Future<Null> tooltip(String message) async { static Future<Null> tooltip(String message) async {
final TooltipSemanticsEvent event = new TooltipSemanticsEvent(message); final TooltipSemanticsEvent event = new TooltipSemanticsEvent(message);
await SystemChannels.accessibility.send(event.toMap()); await SystemChannels.accessibility.send(event.toMap());
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
...@@ -200,4 +201,44 @@ void main() { ...@@ -200,4 +201,44 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(checkBoxValue, null); expect(checkBoxValue, null);
}); });
testWidgets('has semantic events', (WidgetTester tester) async {
dynamic semanticEvent;
bool checkboxValue = false;
SystemChannels.accessibility.setMockMessageHandler((dynamic message) {
semanticEvent = message;
});
final SemanticsTester semanticsTester = new SemanticsTester(tester);
await tester.pumpWidget(
new Material(
child: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return new Checkbox(
value: checkboxValue,
onChanged: (bool value) {
setState(() {
checkboxValue = value;
});
},
);
},
),
),
);
await tester.tap(find.byType(Checkbox));
final RenderObject object = tester.firstRenderObject(find.byType(Checkbox));
expect(checkboxValue, true);
expect(semanticEvent, <String, dynamic>{
'type': 'tap',
'nodeId': object.debugSemantics.id,
'data': <String, dynamic>{},
});
expect(object.debugSemantics.getSemanticsData().hasAction(SemanticsAction.tap), true);
SystemChannels.accessibility.setMockMessageHandler(null);
semanticsTester.dispose();
});
} }
...@@ -2,9 +2,13 @@ ...@@ -2,9 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart'; import 'feedback_tester.dart';
void main () { void main () {
...@@ -21,8 +25,23 @@ void main () { ...@@ -21,8 +25,23 @@ void main () {
}); });
group('Feedback on Android', () { group('Feedback on Android', () {
List<Map<String, Object>> semanticEvents;
setUp(() {
semanticEvents = <Map<String, Object>>[];
SystemChannels.accessibility.setMockMessageHandler((dynamic message) {
final Map<dynamic, dynamic> typedMessage = message;
semanticEvents.add(typedMessage.cast<String, Object>());
});
});
tearDown(() {
SystemChannels.accessibility.setMockMessageHandler(null);
});
testWidgets('forTap', (WidgetTester tester) async { testWidgets('forTap', (WidgetTester tester) async {
final SemanticsTester semanticsTester = new SemanticsTester(tester);
await tester.pumpWidget(new TestWidget( await tester.pumpWidget(new TestWidget(
tapHandler: (BuildContext context) { tapHandler: (BuildContext context) {
return () => Feedback.forTap(context); return () => Feedback.forTap(context);
...@@ -31,14 +50,27 @@ void main () { ...@@ -31,14 +50,27 @@ void main () {
await tester.pumpAndSettle(kWaitDuration); await tester.pumpAndSettle(kWaitDuration);
expect(feedback.hapticCount, 0); expect(feedback.hapticCount, 0);
expect(feedback.clickSoundCount, 0); expect(feedback.clickSoundCount, 0);
expect(semanticEvents, isEmpty);
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pumpAndSettle(kWaitDuration); await tester.pumpAndSettle(kWaitDuration);
final RenderObject object = tester.firstRenderObject(find.byType(GestureDetector));
expect(feedback.hapticCount, 0); expect(feedback.hapticCount, 0);
expect(feedback.clickSoundCount, 1); expect(feedback.clickSoundCount, 1);
expect(semanticEvents.single, <String, dynamic>{
'type': 'tap',
'nodeId': object.debugSemantics.id,
'data': <String, dynamic>{},
});
expect(object.debugSemantics.getSemanticsData().hasAction(SemanticsAction.tap), true);
semanticsTester.dispose();
}); });
testWidgets('forTap Wrapper', (WidgetTester tester) async { testWidgets('forTap Wrapper', (WidgetTester tester) async {
final SemanticsTester semanticsTester = new SemanticsTester(tester);
int callbackCount = 0; int callbackCount = 0;
final VoidCallback callback = () { final VoidCallback callback = () {
callbackCount++; callbackCount++;
...@@ -56,12 +88,24 @@ void main () { ...@@ -56,12 +88,24 @@ void main () {
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pumpAndSettle(kWaitDuration); await tester.pumpAndSettle(kWaitDuration);
final RenderObject object = tester.firstRenderObject(find.byType(GestureDetector));
expect(feedback.hapticCount, 0); expect(feedback.hapticCount, 0);
expect(feedback.clickSoundCount, 1); expect(feedback.clickSoundCount, 1);
expect(callbackCount, 1); expect(callbackCount, 1);
expect(semanticEvents.single, <String, dynamic>{
'type': 'tap',
'nodeId': object.debugSemantics.id,
'data': <String, dynamic>{},
});
expect(object.debugSemantics.getSemanticsData().hasAction(SemanticsAction.tap), true);
semanticsTester.dispose();
}); });
testWidgets('forLongPress', (WidgetTester tester) async { testWidgets('forLongPress', (WidgetTester tester) async {
final SemanticsTester semanticsTester = new SemanticsTester(tester);
await tester.pumpWidget(new TestWidget( await tester.pumpWidget(new TestWidget(
longPressHandler: (BuildContext context) { longPressHandler: (BuildContext context) {
return () => Feedback.forLongPress(context); return () => Feedback.forLongPress(context);
...@@ -73,11 +117,22 @@ void main () { ...@@ -73,11 +117,22 @@ void main () {
await tester.longPress(find.text('X')); await tester.longPress(find.text('X'));
await tester.pumpAndSettle(kWaitDuration); await tester.pumpAndSettle(kWaitDuration);
final RenderObject object = tester.firstRenderObject(find.byType(GestureDetector));
expect(feedback.hapticCount, 1); expect(feedback.hapticCount, 1);
expect(feedback.clickSoundCount, 0); expect(feedback.clickSoundCount, 0);
expect(semanticEvents.single, <String, dynamic>{
'type': 'longPress',
'nodeId': object.debugSemantics.id,
'data': <String, dynamic>{},
});
expect(object.debugSemantics.getSemanticsData().hasAction(SemanticsAction.longPress), true);
semanticsTester.dispose();
}); });
testWidgets('forLongPress Wrapper', (WidgetTester tester) async { testWidgets('forLongPress Wrapper', (WidgetTester tester) async {
final SemanticsTester semanticsTester = new SemanticsTester(tester);
int callbackCount = 0; int callbackCount = 0;
final VoidCallback callback = () { final VoidCallback callback = () {
callbackCount++; callbackCount++;
...@@ -89,6 +144,8 @@ void main () { ...@@ -89,6 +144,8 @@ void main () {
}, },
)); ));
await tester.pumpAndSettle(kWaitDuration); await tester.pumpAndSettle(kWaitDuration);
final RenderObject object = tester.firstRenderObject(find.byType(GestureDetector));
expect(feedback.hapticCount, 0); expect(feedback.hapticCount, 0);
expect(feedback.clickSoundCount, 0); expect(feedback.clickSoundCount, 0);
expect(callbackCount, 0); expect(callbackCount, 0);
...@@ -98,6 +155,14 @@ void main () { ...@@ -98,6 +155,14 @@ void main () {
expect(feedback.hapticCount, 1); expect(feedback.hapticCount, 1);
expect(feedback.clickSoundCount, 0); expect(feedback.clickSoundCount, 0);
expect(callbackCount, 1); expect(callbackCount, 1);
expect(semanticEvents.single, <String, dynamic>{
'type': 'longPress',
'nodeId': object.debugSemantics.id,
'data': <String, dynamic>{},
});
expect(object.debugSemantics.getSemanticsData().hasAction(SemanticsAction.longPress), true);
semanticsTester.dispose();
}); });
}); });
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
...@@ -182,4 +183,40 @@ void main() { ...@@ -182,4 +183,40 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('has semantic events', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
final Key key = new UniqueKey();
dynamic semanticEvent;
int radioValue = 2;
SystemChannels.accessibility.setMockMessageHandler((dynamic message) {
semanticEvent = message;
});
await tester.pumpWidget(new Material(
child: new Radio<int>(
key: key,
value: 1,
groupValue: radioValue,
onChanged: (int i) {
radioValue = i;
},
),
));
await tester.tap(find.byKey(key));
final RenderObject object = tester.firstRenderObject(find.byKey(key));
expect(radioValue, 1);
expect(semanticEvent, <String, dynamic>{
'type': 'tap',
'nodeId': object.debugSemantics.id,
'data': <String, dynamic>{},
});
expect(object.debugSemantics.getSemanticsData().hasAction(SemanticsAction.tap), true);
semantics.dispose();
SystemChannels.accessibility.setMockMessageHandler(null);
});
} }
...@@ -4,9 +4,11 @@ ...@@ -4,9 +4,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
void main() { void main() {
testWidgets('Switch can toggle on tap', (WidgetTester tester) async { testWidgets('Switch can toggle on tap', (WidgetTester tester) async {
...@@ -228,4 +230,48 @@ void main() { ...@@ -228,4 +230,48 @@ void main() {
expect(value, isTrue); expect(value, isTrue);
expect(tester.hasRunningAnimations, false); expect(tester.hasRunningAnimations, false);
}); });
testWidgets('switch has semantic events', (WidgetTester tester) async {
dynamic semanticEvent;
bool value = false;
SystemChannels.accessibility.setMockMessageHandler((dynamic message) {
semanticEvent = message;
});
final SemanticsTester semanticsTester = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return new Material(
child: new Center(
child: new Switch(
value: value,
onChanged: (bool newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
),
);
await tester.tap(find.byType(Switch));
final RenderObject object = tester.firstRenderObject(find.byType(Switch));
expect(value, true);
expect(semanticEvent, <String, dynamic>{
'type': 'tap',
'nodeId': object.debugSemantics.id,
'data': <String, dynamic>{},
});
expect(object.debugSemantics.getSemanticsData().hasAction(SemanticsAction.tap), true);
semanticsTester.dispose();
SystemChannels.accessibility.setMockMessageHandler(null);
});
} }
...@@ -4,6 +4,7 @@ import 'dart:ui'; ...@@ -4,6 +4,7 @@ import 'dart:ui';
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
...@@ -676,4 +677,51 @@ void main() { ...@@ -676,4 +677,51 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('has semantic events', (WidgetTester tester) async {
final List<dynamic> semanticEvents = <dynamic>[];
SystemChannels.accessibility.setMockMessageHandler((dynamic message) {
semanticEvents.add(message);
});
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new MaterialApp(
home: new Center(
child: new Tooltip(
message: 'Foo',
child: new Container(
width: 100.0,
height: 100.0,
color: Colors.green[500],
),
),
),
),
);
await tester.longPress(find.byType(Tooltip));
final RenderObject object = tester.firstRenderObject(find.byType(Tooltip));
expect(semanticEvents, unorderedEquals(<dynamic>[
<String, dynamic>{
'type': 'longPress',
'nodeId': findDebugSemantics(object).id,
'data': <String, dynamic>{},
},
<String, dynamic>{
'type': 'tooltip',
'data': <String, dynamic>{
'message': 'Foo',
},
},
]));
semantics.dispose();
SystemChannels.accessibility.setMockMessageHandler(null);
});
}
SemanticsNode findDebugSemantics(RenderObject object) {
if (object.debugSemantics != null)
return object.debugSemantics;
return findDebugSemantics(object.parent);
} }
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