Identify text fields as such to a11y (#12804)
* Identify text fields as such to a11y * focus * make travis happy * review comments
This commit is contained in:
parent
005a8e4c8e
commit
1d4607ff04
@ -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)})';
|
||||
strings.add(' (${describeEnum(textDirection)})');
|
||||
if (actions != null)
|
||||
string += ' and ';
|
||||
} else if (textDirection != null) {
|
||||
string += 'direction ${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,
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user