Unverified Commit 89d6c8d9 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Enables setting of semantics focused and focusable attributes within Focus widgets. (#41814)

This adds a Semantics node to the Focus and FocusScope widgets, setting the focused and focusable attributes so that the accessibility subsystem can be told when a control has the input focus.

Includes an engine roll to flutter/engine@77252d2, and the following 8 engine changes:

flutter/engine@77252d2 Greg Spencer Add missing focusable testing info (flutter/engine#13013)
flutter/engine@0e42a29 skia-flutter-.. Roll src/third_party/skia 54548626a977..e27a503a0a21 (1 commits) (flutter/engine#13024)
flutter/engine@6b56ed7 gaaclarke Refactor: FlutterDartProject (flutter/engine#13006)
flutter/engine@393480c skia-flutter-.. Roll src/third_party/skia 77dde599c98a..54548626a977 (1 commits) (flutter/engine#13023)
flutter/engine@080b89d skia-flutter-.. Roll src/third_party/skia 2b1a25a4d324..77dde599c98a (1 commits) (flutter/engine#13021)
flutter/engine@90b0f30 Ben Konyi Roll src/third_party/dart f4a72bfc64..bb04f145b2 (18 commits) (flutter/engine#13020)
flutter/engine@049fb89 skia-flutter-.. Roll fuchsia/sdk/core/linux-amd64 from q_uYX... to cknsi... (flutter/engine#13019)
flutter/engine@6925b2a skia-flutter-.. Roll fuchsia/sdk/core/mac-amd64 from wuAtw... to u0JpE... (flutter/engine#13018)

Related Issues
Addresses #40101

Landing on red in order to fix the build: it's red because of the needed engine roll.
parent 3c521fb9
0d749933bcc63b53771e37e6cc28fa43c3d1fd06
77252d2371f3341f9f3a7e3ec2eb64bfa29bf975
......@@ -11,6 +11,8 @@ import 'package:flutter/services.dart';
import 'package:flutter_driver/driver_extension.dart';
import 'src/tests/controls_page.dart';
import 'src/tests/popup_constants.dart';
import 'src/tests/popup_page.dart';
import 'src/tests/text_field_page.dart';
void main() {
......@@ -40,10 +42,11 @@ Future<String> dataHandler(String message) async {
throw UnimplementedError();
}
const List<String> routes = <String>[
selectionControlsRoute,
textFieldRoute,
];
Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
selectionControlsRoute : (BuildContext context) => SelectionControlsPage(),
popupControlsRoute : (BuildContext context) => PopupControlsPage(),
textFieldRoute : (BuildContext context) => TextFieldPage(),
};
class TestApp extends StatelessWidget {
const TestApp();
......@@ -51,15 +54,12 @@ class TestApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
routes: <String, WidgetBuilder>{
selectionControlsRoute: (BuildContext context) => SelectionControlsPage(),
textFieldRoute: (BuildContext context) => TextFieldPage(),
},
routes: routes,
home: Builder(
builder: (BuildContext context) {
return Scaffold(
body: ListView(
children: routes.map<Widget>((String value) {
children: routes.keys.map<Widget>((String value) {
return MaterialButton(
child: Text(value),
onPressed: () {
......
......@@ -24,6 +24,9 @@ class AndroidClassName {
/// The class name used for toggle switches.
static const String toggleSwitch = 'android.widget.Switch';
/// The default className for buttons.
static const String button = 'android.widget.Button';
}
/// Action constants which correspond to `AccessibilityAction` in Android.
......
......@@ -135,8 +135,13 @@ class _AndroidSemanticsMatcher extends Matcher {
return _failWithMessage('Expected size: $size', matchState);
if (actions != null) {
final List<AndroidSemanticsAction> itemActions = item.getActions();
if (!unorderedEquals(actions).matches(itemActions, matchState))
return _failWithMessage('Expected actions: $actions', matchState);
if (!unorderedEquals(actions).matches(itemActions, matchState)) {
final List<String> actionsString = actions.map<String>((AndroidSemanticsAction action) => action.toString()).toList()..sort();
final List<String> itemActionsString = itemActions.map<String>((AndroidSemanticsAction action) => action.toString()).toList()..sort();
final Set<String> unexpected = itemActionsString.toSet().difference(actionsString.toSet());
final Set<String> missing = actionsString.toSet().difference(itemActionsString.toSet());
return _failWithMessage('Expected actions: $actionsString\nActual actions: $itemActionsString\nUnexpected: $unexpected\nMissing: $missing', matchState);
}
}
if (isChecked != null && isChecked != item.isChecked)
return _failWithMessage('Expected isChecked: $isChecked', matchState);
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// The name of the route containing the test suite.
const String popupControlsRoute = 'popups';
/// The string supplied to the [ValueKey] for the popup menu button.
const String popupButtonKeyValue = 'PopupControls#PopupButton1';
/// The string supplied to the [ValueKey] for the popup menu.
const String popupKeyValue = 'PopupControls#Popup1';
/// The string supplied to the [ValueKey] for the dropdown button.
const String dropdownButtonKeyValue = 'PopupControls#DropdownButton1';
/// The string supplied to the [ValueKey] for the dropdown button menu.
const String dropdownKeyValue = 'PopupControls#Dropdown1';
/// The string supplied to the [ValueKey] for the alert button.
const String alertButtonKeyValue = 'PopupControls#AlertButton1';
/// The string supplied to the [ValueKey] for the alert dialog.
const String alertKeyValue = 'PopupControls#Alert1';
const List<String> popupItems = <String>['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'];
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'popup_constants.dart';
export 'popup_constants.dart';
/// A page with a popup menu, a dropdown menu, and a modal alert.
class PopupControlsPage extends StatefulWidget {
@override
State<StatefulWidget> createState() => _PopupControlsPageState();
}
class _PopupControlsPageState extends State<PopupControlsPage> {
final Key popupKey = const ValueKey<String>(popupKeyValue);
final Key dropdownKey = const ValueKey<String>(dropdownKeyValue);
final Key alertKey = const ValueKey<String>(alertKeyValue);
String popupValue = popupItems.first;
String dropdownValue = popupItems.first;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(leading: const BackButton(key: ValueKey<String>('back'))),
body: SafeArea(
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
PopupMenuButton<String>(
key: const ValueKey<String>(popupButtonKeyValue),
icon: const Icon(Icons.arrow_drop_down),
itemBuilder: (BuildContext context) {
return popupItems.map<PopupMenuItem<String>>((String item) {
return PopupMenuItem<String>(
key: ValueKey<String>('$popupKeyValue.$item'),
value: item,
child: Text(item),
);
}).toList();
},
onSelected: (String value) {
popupValue = value;
},
),
DropdownButton<String>(
key: const ValueKey<String>(dropdownButtonKeyValue),
value: dropdownValue,
items: popupItems.map<DropdownMenuItem<String>>((String item) {
return DropdownMenuItem<String>(
key: ValueKey<String>('$dropdownKeyValue.$item'),
value: item,
child: Text(item),
);
}).toList(),
onChanged: (String value) {
setState(() {
dropdownValue = value;
});
},
),
MaterialButton(
key: const ValueKey<String>(alertButtonKeyValue),
child: const Text('Alert'),
onPressed: () {
showDialog<void>(
context: context,
barrierDismissible: false, // user must tap button!
builder: (BuildContext context) {
return AlertDialog(
key: const ValueKey<String>(alertKeyValue),
title: const Text('Title text', key: ValueKey<String>('$alertKeyValue.Title')),
content: SingleChildScrollView(
child: ListBody(
children: const <Widget>[
Text('Body text line 1.', key: ValueKey<String>('$alertKeyValue.Body1')),
Text('Body text line 2.', key: ValueKey<String>('$alertKeyValue.Body2')),
],
),
),
actions: <Widget>[
FlatButton(
child: const Text('OK', key: ValueKey<String>('$alertKeyValue.OK')),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
},
),
],
),
),
),
);
}
}
......@@ -3,4 +3,5 @@
// found in the LICENSE file.
export 'src/tests/controls_constants.dart';
export 'src/tests/popup_constants.dart';
export 'src/tests/text_field_constants.dart';
......@@ -18,6 +18,7 @@ void main() {
isButton: true,
hasEnabledState: true,
isEnabled: true,
isFocusable: true,
hasTapAction: true,
label: 'Update border shape',
));
......@@ -26,6 +27,7 @@ void main() {
isButton: true,
hasEnabledState: true,
isEnabled: true,
isFocusable: true,
hasTapAction: true,
label: 'Reset chips',
));
......
......@@ -723,7 +723,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
} else {
// For the sake of backwards compatibility. An (unlikely) app that relied
// on having menus only inherit from the material Theme could set
// captureInheritedThemes to false and get the original behvaior.
// captureInheritedThemes to false and get the original behavior.
if (theme != null)
menu = Theme(data: theme, child: menu);
}
......
......@@ -840,6 +840,9 @@ class RenderCustomPaint extends RenderProxyBox {
if (properties.readOnly != null) {
config.isReadOnly = properties.readOnly;
}
if (properties.focusable != null) {
config.isFocusable = properties.focusable;
}
if (properties.focused != null) {
config.isFocused = properties.focused;
}
......
......@@ -3489,6 +3489,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
bool header,
bool textField,
bool readOnly,
bool focusable,
bool focused,
bool inMutuallyExclusiveGroup,
bool obscured,
......@@ -3541,6 +3542,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_header = header,
_textField = textField,
_readOnly = readOnly,
_focusable = focusable,
_focused = focused,
_inMutuallyExclusiveGroup = inMutuallyExclusiveGroup,
_obscured = obscured,
......@@ -3720,6 +3722,16 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.isFocusable] semantic to the given value.
bool get focusable => _focusable;
bool _focusable;
set focusable(bool value) {
if (focusable == value)
return;
_focusable = value;
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.isFocused] semantic to the given value.
bool get focused => _focused;
bool _focused;
......@@ -4380,6 +4392,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.isTextField = textField;
if (readOnly != null)
config.isReadOnly = readOnly;
if (focusable != null)
config.isFocusable = focusable;
if (focused != null)
config.isFocused = focused;
if (inMutuallyExclusiveGroup != null)
......
......@@ -595,6 +595,7 @@ class SemanticsProperties extends DiagnosticableTree {
this.header,
this.textField,
this.readOnly,
this.focusable,
this.focused,
this.inMutuallyExclusiveGroup,
this.hidden,
......@@ -698,14 +699,25 @@ class SemanticsProperties extends DiagnosticableTree {
/// TalkBack/VoiceOver will treat it as non-editable text field.
final bool readOnly;
/// If non-null, whether the node is able to hold input focus.
///
/// If [focusable] is set to false, then [focused] must not be true.
///
/// Input focus indicates that the node will receive keyboard events. It is not
/// to be confused with accessibility focus. Accessibility focus is the
/// green/black rectangular highlight that TalkBack/VoiceOver draws around the
/// element it is reading, and is separate from input focus.
final bool focusable;
/// If non-null, whether the node currently holds input focus.
///
/// At most one node in the tree should hold input focus at any point in time.
/// At most one node in the tree should hold input focus at any point in time,
/// and it should not be set to true if [focusable] is false.
///
/// Input focus (indicates that the node will receive keyboard events) is not
/// Input focus indicates that the node will receive keyboard events. It is not
/// to be confused with accessibility focus. Accessibility focus is the
/// green/black rectangular that TalkBack/VoiceOver on the screen and is
/// separate from input focus.
/// green/black rectangular highlight that TalkBack/VoiceOver draws around the
/// element it is reading, and is separate from input focus.
final bool focused;
/// If non-null, whether a semantic node is in a mutually exclusive group.
......@@ -3611,7 +3623,13 @@ class SemanticsConfiguration {
_setFlag(SemanticsFlag.isInMutuallyExclusiveGroup, value);
}
/// Whether the owning [RenderObject] currently holds the user's focus.
/// Whether the owning [RenderObject] can hold the input focus.
bool get isFocusable => _hasFlag(SemanticsFlag.isFocusable);
set isFocusable(bool value) {
_setFlag(SemanticsFlag.isFocusable, value);
}
/// Whether the owning [RenderObject] currently holds the input focus.
bool get isFocused => _hasFlag(SemanticsFlag.isFocused);
set isFocused(bool value) {
_setFlag(SemanticsFlag.isFocused, value);
......
......@@ -6188,6 +6188,7 @@ class Semantics extends SingleChildRenderObjectWidget {
bool header,
bool textField,
bool readOnly,
bool focusable,
bool focused,
bool inMutuallyExclusiveGroup,
bool obscured,
......@@ -6242,6 +6243,7 @@ class Semantics extends SingleChildRenderObjectWidget {
header: header,
textField: textField,
readOnly: readOnly,
focusable: focusable,
focused: focused,
inMutuallyExclusiveGroup: inMutuallyExclusiveGroup,
obscured: obscured,
......@@ -6355,6 +6357,7 @@ class Semantics extends SingleChildRenderObjectWidget {
header: properties.header,
textField: properties.textField,
readOnly: properties.readOnly,
focusable: properties.focusable,
focused: properties.focused,
liveRegion: properties.liveRegion,
maxValueLength: properties.maxValueLength,
......@@ -6425,6 +6428,7 @@ class Semantics extends SingleChildRenderObjectWidget {
..header = properties.header
..textField = properties.textField
..readOnly = properties.readOnly
..focusable = properties.focusable
..focused = properties.focused
..inMutuallyExclusiveGroup = properties.inMutuallyExclusiveGroup
..obscured = properties.obscured
......
......@@ -137,7 +137,7 @@ class Focus extends StatefulWidget {
///
/// The [child] argument is required and must not be null.
///
/// The [autofocus] and [skipTraversal] arguments must not be null.
/// The [autofocus] argument must not be null.
const Focus({
Key key,
@required this.child,
......@@ -323,6 +323,8 @@ class _FocusState extends State<Focus> {
FocusNode _internalNode;
FocusNode get focusNode => widget.focusNode ?? _internalNode;
bool _hasFocus;
bool _hasPrimaryFocus;
bool _canRequestFocus;
bool _didAutofocus = false;
FocusAttachment _focusAttachment;
......@@ -343,6 +345,8 @@ class _FocusState extends State<Focus> {
focusNode.skipTraversal = widget.skipTraversal ?? focusNode.skipTraversal;
focusNode.canRequestFocus = widget.canRequestFocus ?? focusNode.canRequestFocus;
_hasFocus = focusNode.hasFocus;
_canRequestFocus = focusNode.canRequestFocus;
_hasPrimaryFocus = focusNode.hasPrimaryFocus;
// Add listener even if the _internalNode existed before, since it should
// not be listening now if we're re-using a previous one because it should
......@@ -426,6 +430,16 @@ class _FocusState extends State<Focus> {
widget.onFocusChange(focusNode.hasFocus);
}
}
if (_hasPrimaryFocus != focusNode.hasPrimaryFocus) {
setState(() {
_hasPrimaryFocus = focusNode.hasPrimaryFocus;
});
}
if (_canRequestFocus != focusNode.canRequestFocus) {
setState(() {
_canRequestFocus = focusNode.canRequestFocus;
});
}
}
@override
......@@ -433,7 +447,11 @@ class _FocusState extends State<Focus> {
_focusAttachment.reparent();
return _FocusMarker(
node: focusNode,
child: widget.child,
child: Semantics(
focusable: _canRequestFocus,
focused: _hasPrimaryFocus,
child: widget.child,
),
);
}
}
......
......@@ -119,6 +119,7 @@ void main() {
hasEnabledState: true,
isEnabled: true,
hasTapAction: true,
isFocusable: true,
));
handle.dispose();
});
......
......@@ -1191,6 +1191,7 @@ void main() {
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isFocusable,
SemanticsFlag.isSelected,
],
actions: <SemanticsAction>[SemanticsAction.tap],
......@@ -1198,11 +1199,13 @@ void main() {
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Alarm\nTab 2 of 3',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Hot Tub\nTab 3 of 3',
textDirection: TextDirection.ltr,
......@@ -1252,6 +1255,7 @@ void main() {
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isFocusable,
SemanticsFlag.isSelected,
],
actions: <SemanticsAction>[SemanticsAction.tap],
......@@ -1259,11 +1263,13 @@ void main() {
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Alarm\nTab 2 of 3',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Hot Tub\nTab 3 of 3',
textDirection: TextDirection.ltr,
......@@ -1553,6 +1559,7 @@ void main() {
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isFocusable,
SemanticsFlag.isSelected,
],
actions: <SemanticsAction>[SemanticsAction.tap],
......@@ -1560,6 +1567,7 @@ void main() {
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Green\nTab 2 of 2',
textDirection: TextDirection.ltr,
......@@ -1608,12 +1616,14 @@ void main() {
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isSelected,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Red\nTab 1 of 2',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Green\nTab 2 of 2',
textDirection: TextDirection.ltr,
......
......@@ -565,9 +565,10 @@ void main() {
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
transform: Matrix4.translationValues(356.0, 276.0, 0.0),
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
),
],
......@@ -605,9 +606,10 @@ void main() {
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
transform: Matrix4.translationValues(356.0, 276.0, 0.0),
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
),
],
......@@ -889,9 +891,10 @@ void main() {
SemanticsAction.tap,
],
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
),
],
......@@ -920,8 +923,9 @@ void main() {
transform: expectedButtonTransform,
label: 'Button',
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isFocusable,
],
),
],
......
......@@ -62,9 +62,10 @@ void main() {
SemanticsAction.tap,
],
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
),
],
......
......@@ -1353,6 +1353,7 @@ void main() {
TestSemantics(
label: 'test',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
),
],
),
......@@ -1390,6 +1391,7 @@ void main() {
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
),
......@@ -1436,6 +1438,7 @@ void main() {
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
),
......@@ -1474,9 +1477,10 @@ void main() {
label: 'test',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[
SemanticsFlag.isSelected,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
SemanticsFlag.isSelected,
],
actions: <SemanticsAction>[SemanticsAction.tap],
),
......
......@@ -636,25 +636,45 @@ void _tests() {
],
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isButton, SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled],
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Previous month December 2015',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isButton, SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled],
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Next month February 2016',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isButton, SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled],
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'CANCEL',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isButton, SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled],
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'OK',
textDirection: TextDirection.ltr,
......
......@@ -148,6 +148,7 @@ void main() {
hasTapAction: true,
hasEnabledState: true,
isEnabled: true,
isFocusable: true,
isButton: true,
onTapHint: localizations.expandedIconTapHint,
));
......@@ -163,6 +164,7 @@ void main() {
hasTapAction: true,
hasEnabledState: true,
isEnabled: true,
isFocusable: true,
isButton: true,
onTapHint: localizations.collapsedIconTapHint,
));
......
......@@ -194,6 +194,7 @@ void main() {
isButton: true,
hasEnabledState: true,
isEnabled: true,
isFocusable: true,
hasTapAction: true,
));
......@@ -937,6 +938,7 @@ void main() {
isButton: true,
hasEnabledState: true,
isEnabled: true,
isFocusable: true,
hasTapAction: true,
onTapHint: localizations.expandedIconTapHint,
));
......@@ -960,6 +962,7 @@ void main() {
isButton: true,
hasEnabledState: true,
isEnabled: true,
isFocusable: true,
hasTapAction: true,
onTapHint: localizations.collapsedIconTapHint,
));
......
......@@ -560,9 +560,10 @@ void main() {
TestSemantics.rootChild(
label: 'Add',
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
......@@ -634,9 +635,10 @@ void main() {
SemanticsAction.tap,
],
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
),
],
......@@ -780,6 +782,7 @@ void main() {
hasEnabledState: true,
isButton: true,
isEnabled: true,
isFocusable: true,
),
);
}, semanticsEnabled: true);
......
......@@ -294,8 +294,9 @@ void main() {
],
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
label: 'link',
),
......
......@@ -666,9 +666,10 @@ void main() {
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
transform: Matrix4.translationValues(356.0, 276.0, 0.0),
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
),
],
......
......@@ -55,9 +55,10 @@ void main() {
TestSemantics(
id: 1,
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
......
......@@ -558,9 +558,10 @@ void main() {
TestSemantics(
id: 10,
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Back',
......@@ -588,9 +589,10 @@ void main() {
TestSemantics(
id: 8,
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Suggestions',
......
......@@ -417,6 +417,7 @@ void _defineTests() {
textField: true,
readOnly: true,
focused: true,
focusable: true,
inMutuallyExclusiveGroup: true,
header: true,
obscured: true,
......@@ -431,6 +432,7 @@ void _defineTests() {
),
));
List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
print('flags: $flags');
// [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
// therefore it has to be removed.
flags.remove(SemanticsFlag.hasImplicitScrolling);
......@@ -466,6 +468,7 @@ void _defineTests() {
textField: true,
readOnly: true,
focused: true,
focusable: true,
inMutuallyExclusiveGroup: true,
header: true,
obscured: true,
......@@ -482,7 +485,6 @@ void _defineTests() {
// [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
// therefore it has to be removed.
flags.remove(SemanticsFlag.hasImplicitScrolling);
expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
......
......@@ -2,8 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/semantics.dart';
class TestFocus extends StatefulWidget {
const TestFocus({
......@@ -24,6 +27,7 @@ class TestFocus extends StatefulWidget {
class TestFocusState extends State<TestFocus> {
FocusNode focusNode;
String _label;
bool built = false;
@override
void dispose() {
......@@ -50,6 +54,7 @@ class TestFocusState extends State<TestFocus> {
@override
Widget build(BuildContext context) {
built = true;
return GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(focusNode);
......@@ -1290,4 +1295,48 @@ void main() {
expect(WidgetsBinding.instance.focusManager.rootScope.descendants, isEmpty);
});
testWidgets('Focus widgets set Semantics information about focus', (WidgetTester tester) async {
final GlobalKey<TestFocusState> key = GlobalKey();
await tester.pumpWidget(
TestFocus(key: key, name: 'a'),
);
final SemanticsNode semantics = tester.getSemantics(find.byKey(key));
expect(key.currentState.focusNode.hasFocus, isFalse);
expect(semantics.hasFlag(SemanticsFlag.isFocused), isFalse);
expect(semantics.hasFlag(SemanticsFlag.isFocusable), isTrue);
FocusScope.of(key.currentContext).requestFocus(key.currentState.focusNode);
await tester.pumpAndSettle();
expect(key.currentState.focusNode.hasFocus, isTrue);
expect(semantics.hasFlag(SemanticsFlag.isFocused), isTrue);
expect(semantics.hasFlag(SemanticsFlag.isFocusable), isTrue);
key.currentState.focusNode.canRequestFocus = false;
await tester.pumpAndSettle();
expect(key.currentState.focusNode.hasFocus, isFalse);
expect(key.currentState.focusNode.canRequestFocus, isFalse);
expect(semantics.hasFlag(SemanticsFlag.isFocused), isFalse);
expect(semantics.hasFlag(SemanticsFlag.isFocusable), isFalse);
});
testWidgets('Setting canRequestFocus on focus node causes update.', (WidgetTester tester) async {
final GlobalKey<TestFocusState> key = GlobalKey();
final TestFocus testFocus = TestFocus(key: key, name: 'a');
await tester.pumpWidget(
testFocus,
);
await tester.pumpAndSettle();
key.currentState.built = false;
key.currentState.focusNode.canRequestFocus = false;
await tester.pumpAndSettle();
key.currentState.built = true;
expect(key.currentState.focusNode.canRequestFocus, isFalse);
});
}
......@@ -4,6 +4,7 @@
@TestOn('!chrome')
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
......@@ -836,7 +837,15 @@ void main() {
),
);
final SemanticsNode semantics = tester.getSemantics(find.byType(AndroidView));
// Find the first _AndroidPlatformView widget inside of the AndroidView so
// that it finds the right RenderObject when looking for semantics.
final Finder semanticsFinder = find.byWidgetPredicate(
(Widget widget) {
return widget.runtimeType.toString() == '_AndroidPlatformView';
},
description: '_AndroidPlatformView widget inside AndroidView',
);
final SemanticsNode semantics = tester.getSemantics(semanticsFinder.first);
// Platform view has not been created yet, no platformViewId.
expect(semantics.platformViewId, null);
......
......@@ -477,6 +477,7 @@ void main() {
textField: true,
readOnly: true,
focused: true,
focusable: true,
inMutuallyExclusiveGroup: true,
header: true,
obscured: true,
......
......@@ -428,7 +428,11 @@ class FlutterDriverExtension {
Future<GetSemanticsIdResult> _getSemanticsId(Command command) async {
final GetSemanticsId semanticsCommand = command;
final Finder target = await _waitForElement(_createFinder(semanticsCommand.finder));
final Element element = target.evaluate().single;
final Iterable<Element> elements = target.evaluate();
if (elements.length > 1) {
throw StateError('Found more than one element with the same ID: $elements');
}
final Element element = elements.single;
RenderObject renderObject = element.renderObject;
SemanticsNode node;
while (renderObject != null && node == null) {
......
......@@ -494,7 +494,7 @@ void main() {
final Map<String, Object> response = await extension.call(arguments);
expect(response['isError'], true);
expect(response['response'], contains('Bad state: Too many elements'));
expect(response['response'], contains('Bad state: Found more than one element with the same ID'));
semantics.dispose();
});
});
......
......@@ -442,6 +442,7 @@ Matcher matchesSemantics({
bool isButton = false,
bool isLink = false,
bool isFocused = false,
bool isFocusable = false,
bool isTextField = false,
bool isReadOnly = false,
bool hasEnabledState = false,
......@@ -494,6 +495,7 @@ Matcher matchesSemantics({
if (isTextField) SemanticsFlag.isTextField,
if (isReadOnly) SemanticsFlag.isReadOnly,
if (isFocused) SemanticsFlag.isFocused,
if (isFocusable) SemanticsFlag.isFocusable,
if (hasEnabledState) SemanticsFlag.hasEnabledState,
if (isEnabled) SemanticsFlag.isEnabled,
if (isInMutuallyExclusiveGroup) SemanticsFlag.isInMutuallyExclusiveGroup,
......
......@@ -553,6 +553,7 @@ void main() {
isReadOnly: true,
hasEnabledState: true,
isFocused: true,
isFocusable: true,
isEnabled: true,
isInMutuallyExclusiveGroup: true,
isHeader: true,
......
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