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';
import 'box.dart';
import 'object.dart';
import 'semantics.dart';
import 'viewport_offset.dart';
const double _kCaretGap = 1.0; // pixels
......@@ -105,6 +106,7 @@ class RenderEditable extends RenderBox {
TextAlign textAlign: TextAlign.start,
Color cursorColor,
ValueNotifier<bool> showCursor,
bool hasFocus,
int maxLines: 1,
Color selectionColor,
double textScaleFactor: 1.0,
......@@ -125,6 +127,7 @@ class RenderEditable extends RenderBox {
),
_cursorColor = cursorColor,
_showCursor = showCursor ?? new ValueNotifier<bool>(false),
_hasFocus = hasFocus ?? false,
_maxLines = maxLines,
_selection = selection,
_offset = offset {
......@@ -227,6 +230,17 @@ class RenderEditable extends RenderBox {
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.
///
/// If this is 1 (the default), the text will not wrap, but will extend
......@@ -303,6 +317,15 @@ class RenderEditable extends RenderBox {
markNeedsLayout();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config
..isFocused = hasFocus
..isTextField = true;
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
......
......@@ -783,7 +783,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
if (_hasFlag(SemanticsFlags.hasCheckedState))
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('isFocused', value: _hasFlag(SemanticsFlags.isFocused), ifTrue: 'focused'));
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('value', _value, defaultValue: ''));
properties.add(new StringProperty('increasedValue', _increasedValue, defaultValue: ''));
......@@ -1233,11 +1235,21 @@ class SemanticsConfiguration {
_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).
set isButton(bool value) {
_setFlag(SemanticsFlags.isButton, value);
}
/// Whether the owning [RenderObject] is a text field.
set isTextField(bool value) {
_setFlag(SemanticsFlags.isTextField, value);
}
// TAGS
/// The set of tags that this configuration wants to add to all child
......
......@@ -638,6 +638,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
style: widget.style,
cursorColor: widget.cursorColor,
showCursor: _showCursor,
hasFocus: _hasFocus,
maxLines: widget.maxLines,
selectionColor: widget.selectionColor,
textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0,
......@@ -663,6 +664,7 @@ class _Editable extends LeafRenderObjectWidget {
this.style,
this.cursorColor,
this.showCursor,
this.hasFocus,
this.maxLines,
this.selectionColor,
this.textScaleFactor,
......@@ -681,6 +683,7 @@ class _Editable extends LeafRenderObjectWidget {
final TextStyle style;
final Color cursorColor;
final ValueNotifier<bool> showCursor;
final bool hasFocus;
final int maxLines;
final Color selectionColor;
final double textScaleFactor;
......@@ -699,6 +702,7 @@ class _Editable extends LeafRenderObjectWidget {
text: _styledTextSpan,
cursorColor: cursorColor,
showCursor: showCursor,
hasFocus: hasFocus,
maxLines: maxLines,
selectionColor: selectionColor,
textScaleFactor: textScaleFactor,
......@@ -717,6 +721,7 @@ class _Editable extends LeafRenderObjectWidget {
..text = _styledTextSpan
..cursorColor = cursorColor
..showCursor = showCursor
..hasFocus = hasFocus
..maxLines = maxLines
..selectionColor = selectionColor
..textScaleFactor = textScaleFactor
......
......@@ -3,12 +3,14 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui' show SemanticsFlags;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart';
class MockClipboard {
......@@ -1637,4 +1639,25 @@ void main() {
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() {
expect(
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()
......
......@@ -2,11 +2,16 @@
// Use of this source code is governed by a BSD-style license that can be
// 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/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart';
import 'semantics_tester.dart';
void main() {
final TextEditingController controller = new TextEditingController();
final FocusNode focusNode = new FocusNode();
......@@ -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 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show SemanticsFlags;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -314,11 +316,13 @@ class _IncludesNodeWith extends Matcher {
this.label,
this.textDirection,
this.actions,
}) : assert(label != null || actions != null);
this.flags,
}) : assert(label != null || actions != null || flags != null);
final String label;
final TextDirection textDirection;
final List<SemanticsAction> actions;
final List<SemanticsFlags> flags;
@override
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
......@@ -348,6 +352,12 @@ class _IncludesNodeWith extends Matcher {
if (expectedActions != actualActions)
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;
}
......@@ -362,22 +372,16 @@ class _IncludesNodeWith extends Matcher {
}
String get _configAsString {
String string = '';
if (label != null) {
string += 'label "$label"';
final List<String> strings = <String>[];
if (label != null)
strings.add('label "$label"');
if (textDirection != null)
string += ' (${describeEnum(textDirection)})';
if (actions != null)
string += ' and ';
} else if (textDirection != null) {
string += 'direction ${describeEnum(textDirection)}';
strings.add(' (${describeEnum(textDirection)})');
if (actions != null)
string += ' and ';
}
if (actions != null) {
string += 'actions "${actions.join(', ')}"';
}
return string;
strings.add('actions "${actions.join(', ')}"');
if (flags != null)
strings.add('flags "${flags.join(', ')}"');
return strings.join(', ');
}
}
......@@ -385,10 +389,16 @@ class _IncludesNodeWith extends Matcher {
/// `textDirection`, and `actions`.
///
/// 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(
label: label,
textDirection: textDirection,
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