Unverified Commit 1d4607ff authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Identify text fields as such to a11y (#12804)

* Identify text fields as such to a11y

* focus

* make travis happy

* review comments
parent 005a8e4c
...@@ -11,6 +11,7 @@ import 'package:flutter/services.dart'; ...@@ -11,6 +11,7 @@ import 'package:flutter/services.dart';
import 'box.dart'; import 'box.dart';
import 'object.dart'; import 'object.dart';
import 'semantics.dart';
import 'viewport_offset.dart'; import 'viewport_offset.dart';
const double _kCaretGap = 1.0; // pixels const double _kCaretGap = 1.0; // pixels
...@@ -105,6 +106,7 @@ class RenderEditable extends RenderBox { ...@@ -105,6 +106,7 @@ class RenderEditable extends RenderBox {
TextAlign textAlign: TextAlign.start, TextAlign textAlign: TextAlign.start,
Color cursorColor, Color cursorColor,
ValueNotifier<bool> showCursor, ValueNotifier<bool> showCursor,
bool hasFocus,
int maxLines: 1, int maxLines: 1,
Color selectionColor, Color selectionColor,
double textScaleFactor: 1.0, double textScaleFactor: 1.0,
...@@ -125,6 +127,7 @@ class RenderEditable extends RenderBox { ...@@ -125,6 +127,7 @@ class RenderEditable extends RenderBox {
), ),
_cursorColor = cursorColor, _cursorColor = cursorColor,
_showCursor = showCursor ?? new ValueNotifier<bool>(false), _showCursor = showCursor ?? new ValueNotifier<bool>(false),
_hasFocus = hasFocus ?? false,
_maxLines = maxLines, _maxLines = maxLines,
_selection = selection, _selection = selection,
_offset = offset { _offset = offset {
...@@ -227,6 +230,17 @@ class RenderEditable extends RenderBox { ...@@ -227,6 +230,17 @@ class RenderEditable extends RenderBox {
markNeedsPaint(); markNeedsPaint();
} }
/// Whether the editable is currently focused.
bool get hasFocus => _hasFocus;
bool _hasFocus;
set hasFocus(bool value) {
assert(value != null);
if (_hasFocus == value)
return;
_hasFocus = value;
markNeedsSemanticsUpdate();
}
/// The maximum number of lines for the text to span, wrapping if necessary. /// The maximum number of lines for the text to span, wrapping if necessary.
/// ///
/// If this is 1 (the default), the text will not wrap, but will extend /// If this is 1 (the default), the text will not wrap, but will extend
...@@ -303,6 +317,15 @@ class RenderEditable extends RenderBox { ...@@ -303,6 +317,15 @@ class RenderEditable extends RenderBox {
markNeedsLayout(); markNeedsLayout();
} }
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config
..isFocused = hasFocus
..isTextField = true;
}
@override @override
void attach(PipelineOwner owner) { void attach(PipelineOwner owner) {
super.attach(owner); super.attach(owner);
......
...@@ -783,7 +783,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -783,7 +783,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
if (_hasFlag(SemanticsFlags.hasCheckedState)) if (_hasFlag(SemanticsFlags.hasCheckedState))
properties.add(new FlagProperty('isChecked', value: _hasFlag(SemanticsFlags.isChecked), ifTrue: 'checked', ifFalse: 'unchecked')); properties.add(new FlagProperty('isChecked', value: _hasFlag(SemanticsFlags.isChecked), ifTrue: 'checked', ifFalse: 'unchecked'));
properties.add(new FlagProperty('isSelected', value: _hasFlag(SemanticsFlags.isSelected), ifTrue: 'selected')); properties.add(new FlagProperty('isSelected', value: _hasFlag(SemanticsFlags.isSelected), ifTrue: 'selected'));
properties.add(new FlagProperty('isFocused', value: _hasFlag(SemanticsFlags.isFocused), ifTrue: 'focused'));
properties.add(new FlagProperty('isButton', value: _hasFlag(SemanticsFlags.isButton), ifTrue: 'button')); properties.add(new FlagProperty('isButton', value: _hasFlag(SemanticsFlags.isButton), ifTrue: 'button'));
properties.add(new FlagProperty('isTextField', value: _hasFlag(SemanticsFlags.isTextField), ifTrue: 'textField'));
properties.add(new StringProperty('label', _label, defaultValue: '')); properties.add(new StringProperty('label', _label, defaultValue: ''));
properties.add(new StringProperty('value', _value, defaultValue: '')); properties.add(new StringProperty('value', _value, defaultValue: ''));
properties.add(new StringProperty('increasedValue', _increasedValue, defaultValue: '')); properties.add(new StringProperty('increasedValue', _increasedValue, defaultValue: ''));
...@@ -1233,11 +1235,21 @@ class SemanticsConfiguration { ...@@ -1233,11 +1235,21 @@ class SemanticsConfiguration {
_setFlag(SemanticsFlags.isChecked, value); _setFlag(SemanticsFlags.isChecked, value);
} }
/// Whether the owning [RenderObject] currently holds the user's focus.
set isFocused(bool value) {
_setFlag(SemanticsFlags.isFocused, value);
}
/// Whether the owning [RenderObject] is a button (true) or not (false). /// Whether the owning [RenderObject] is a button (true) or not (false).
set isButton(bool value) { set isButton(bool value) {
_setFlag(SemanticsFlags.isButton, value); _setFlag(SemanticsFlags.isButton, value);
} }
/// Whether the owning [RenderObject] is a text field.
set isTextField(bool value) {
_setFlag(SemanticsFlags.isTextField, value);
}
// TAGS // TAGS
/// The set of tags that this configuration wants to add to all child /// The set of tags that this configuration wants to add to all child
......
...@@ -638,6 +638,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -638,6 +638,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
style: widget.style, style: widget.style,
cursorColor: widget.cursorColor, cursorColor: widget.cursorColor,
showCursor: _showCursor, showCursor: _showCursor,
hasFocus: _hasFocus,
maxLines: widget.maxLines, maxLines: widget.maxLines,
selectionColor: widget.selectionColor, selectionColor: widget.selectionColor,
textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0, textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0,
...@@ -663,6 +664,7 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -663,6 +664,7 @@ class _Editable extends LeafRenderObjectWidget {
this.style, this.style,
this.cursorColor, this.cursorColor,
this.showCursor, this.showCursor,
this.hasFocus,
this.maxLines, this.maxLines,
this.selectionColor, this.selectionColor,
this.textScaleFactor, this.textScaleFactor,
...@@ -681,6 +683,7 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -681,6 +683,7 @@ class _Editable extends LeafRenderObjectWidget {
final TextStyle style; final TextStyle style;
final Color cursorColor; final Color cursorColor;
final ValueNotifier<bool> showCursor; final ValueNotifier<bool> showCursor;
final bool hasFocus;
final int maxLines; final int maxLines;
final Color selectionColor; final Color selectionColor;
final double textScaleFactor; final double textScaleFactor;
...@@ -699,6 +702,7 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -699,6 +702,7 @@ class _Editable extends LeafRenderObjectWidget {
text: _styledTextSpan, text: _styledTextSpan,
cursorColor: cursorColor, cursorColor: cursorColor,
showCursor: showCursor, showCursor: showCursor,
hasFocus: hasFocus,
maxLines: maxLines, maxLines: maxLines,
selectionColor: selectionColor, selectionColor: selectionColor,
textScaleFactor: textScaleFactor, textScaleFactor: textScaleFactor,
...@@ -717,6 +721,7 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -717,6 +721,7 @@ class _Editable extends LeafRenderObjectWidget {
..text = _styledTextSpan ..text = _styledTextSpan
..cursorColor = cursorColor ..cursorColor = cursorColor
..showCursor = showCursor ..showCursor = showCursor
..hasFocus = hasFocus
..maxLines = maxLines ..maxLines = maxLines
..selectionColor = selectionColor ..selectionColor = selectionColor
..textScaleFactor = textScaleFactor ..textScaleFactor = textScaleFactor
......
...@@ -3,12 +3,14 @@ ...@@ -3,12 +3,14 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:ui' show SemanticsFlags;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart'; import 'feedback_tester.dart';
class MockClipboard { class MockClipboard {
...@@ -1637,4 +1639,25 @@ void main() { ...@@ -1637,4 +1639,25 @@ void main() {
expect(find.text('5 / 10'), findsOneWidget); expect(find.text('5 / 10'), findsOneWidget);
}); });
testWidgets('TextField identifies as text field in semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new MaterialApp(
home: const Material(
child: const DefaultTextStyle(
style: const TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
child: const Center(
child: const TextField(
maxLength: 10,
),
),
),
),
),
);
expect(semantics, includesNodeWith(flags: <SemanticsFlags>[SemanticsFlags.isTextField]));
});
} }
...@@ -198,7 +198,7 @@ void main() { ...@@ -198,7 +198,7 @@ void main() {
expect( expect(
minimalProperties.toStringDeep(minLevel: DiagnosticLevel.hidden), minimalProperties.toStringDeep(minLevel: DiagnosticLevel.hidden),
'SemanticsNode#16(owner: null, isPartOfNodeMerging: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), actions: [], isSelected: false, isButton: false, label: "", value: "", increasedValue: "", decreasedValue: "", hint: "", textDirection: null)\n', 'SemanticsNode#16(owner: null, isPartOfNodeMerging: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), actions: [], isSelected: false, isFocused: false, isButton: false, isTextField: false, label: "", value: "", increasedValue: "", decreasedValue: "", hint: "", textDirection: null)\n'
); );
final SemanticsConfiguration config = new SemanticsConfiguration() final SemanticsConfiguration config = new SemanticsConfiguration()
......
...@@ -2,11 +2,16 @@ ...@@ -2,11 +2,16 @@
// 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' show SemanticsFlags;
import 'package:flutter/rendering.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';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'semantics_tester.dart';
void main() { void main() {
final TextEditingController controller = new TextEditingController(); final TextEditingController controller = new TextEditingController();
final FocusNode focusNode = new FocusNode(); final FocusNode focusNode = new FocusNode();
...@@ -250,4 +255,33 @@ void main() { ...@@ -250,4 +255,33 @@ void main() {
}), }),
]); ]);
}); });
testWidgets('EditableText identifies as text field (w/ focus) in semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new FocusScope(
node: focusScopeNode,
autofocus: true,
child: new EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
expect(semantics, includesNodeWith(flags: <SemanticsFlags>[SemanticsFlags.isTextField]));
await tester.tap(find.byType(EditableText));
await tester.idle();
await tester.pump();
expect(semantics, includesNodeWith(flags: <SemanticsFlags>[SemanticsFlags.isTextField, SemanticsFlags.isFocused]));
});
} }
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// 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' show SemanticsFlags;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -314,11 +316,13 @@ class _IncludesNodeWith extends Matcher { ...@@ -314,11 +316,13 @@ class _IncludesNodeWith extends Matcher {
this.label, this.label,
this.textDirection, this.textDirection,
this.actions, this.actions,
}) : assert(label != null || actions != null); this.flags,
}) : assert(label != null || actions != null || flags != null);
final String label; final String label;
final TextDirection textDirection; final TextDirection textDirection;
final List<SemanticsAction> actions; final List<SemanticsAction> actions;
final List<SemanticsFlags> flags;
@override @override
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) { bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
...@@ -348,6 +352,12 @@ class _IncludesNodeWith extends Matcher { ...@@ -348,6 +352,12 @@ class _IncludesNodeWith extends Matcher {
if (expectedActions != actualActions) if (expectedActions != actualActions)
return false; return false;
} }
if (flags != null) {
final int expectedFlags = flags.fold(0, (int value, SemanticsFlags flag) => value | flag.index);
final int actualFlags = node.getSemanticsData().flags;
if (expectedFlags != actualFlags)
return false;
}
return true; return true;
} }
...@@ -362,22 +372,16 @@ class _IncludesNodeWith extends Matcher { ...@@ -362,22 +372,16 @@ class _IncludesNodeWith extends Matcher {
} }
String get _configAsString { String get _configAsString {
String string = ''; final List<String> strings = <String>[];
if (label != null) { if (label != null)
string += 'label "$label"'; strings.add('label "$label"');
if (textDirection != null) if (textDirection != null)
string += ' (${describeEnum(textDirection)})'; strings.add(' (${describeEnum(textDirection)})');
if (actions != null)
string += ' and ';
} else if (textDirection != null) {
string += 'direction ${describeEnum(textDirection)}';
if (actions != null) if (actions != null)
string += ' and '; strings.add('actions "${actions.join(', ')}"');
} if (flags != null)
if (actions != null) { strings.add('flags "${flags.join(', ')}"');
string += 'actions "${actions.join(', ')}"'; return strings.join(', ');
}
return string;
} }
} }
...@@ -385,10 +389,16 @@ class _IncludesNodeWith extends Matcher { ...@@ -385,10 +389,16 @@ class _IncludesNodeWith extends Matcher {
/// `textDirection`, and `actions`. /// `textDirection`, and `actions`.
/// ///
/// If null is provided for an argument, it will match against any value. /// If null is provided for an argument, it will match against any value.
Matcher includesNodeWith({ String label, TextDirection textDirection, List<SemanticsAction> actions }) { Matcher includesNodeWith({
String label,
TextDirection textDirection,
List<SemanticsAction> actions,
List<SemanticsFlags> flags,
}) {
return new _IncludesNodeWith( return new _IncludesNodeWith(
label: label, label: label,
textDirection: textDirection, textDirection: textDirection,
actions: actions, actions: actions,
flags: flags,
); );
} }
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