Commit c3e04961 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Support for accessibilityHint and accessibilityValue (#12677)

* Support accessibility labels and hints

* more tests

* ++

* review comments

* fix merge

* test fix
parent 80b820a2
8e79156765c67b71b1e1b9895dbc8eea43f9949c
91071f817b2f6a0f6684e1f2fda3d8b21314bcb7
......@@ -3176,6 +3176,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
bool selected,
bool button,
String label,
String value,
String hint,
TextDirection textDirection,
}) : assert(container != null),
_container = container,
......@@ -3184,6 +3186,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_selected = selected,
_button = button,
_label = label,
_value = value,
_hint = hint,
_textDirection = textDirection,
super(child);
......@@ -3250,31 +3254,59 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue);
}
/// If non-null, sets the [SemanticsNode.isButton] semantic to the given value.
bool get button => _button;
bool _button;
set button(bool value) {
if (button == value)
return;
final bool hadValue = button != null;
_button = value;
markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue);
}
/// If non-null, sets the [SemanticsNode.label] semantic to the given value.
///
/// The text's reading direction is given by [textDirection].
String get label => _label;
String _label;
set label(String value) {
if (label == value)
if (_label == value)
return;
final bool hadValue = label != null;
final bool hadValue = _label != null;
_label = value;
markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue);
}
/// If non-null, sets the [SemanticsNode.isButton] semantic to the given value.
bool get button => _button;
bool _button;
set button(bool value) {
if (button == value)
/// If non-null, sets the [SemanticsNode.value] semantic to the given value.
///
/// The text's reading direction is given by [textDirection].
String get value => _value;
String _value;
set value(String value) {
if (_value == value)
return;
final bool hadValue = button != null;
_button = value;
final bool hadValue = _value != null;
_value = value;
markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue);
}
/// If non-null, sets the [SemanticsNode.hint] semantic to the given value.
///
/// The text's reading direction is given by [textDirection].
String get hint => _hint;
String _hint;
set hint(String value) {
if (_hint == value)
return;
final bool hadValue = _hint != null;
_hint = value;
markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue);
}
/// If non-null, sets the [SemanticsNode.textDirection] semantic to the given value.
///
/// This must not be null if [label] is not null.
/// This must not be null if [label], [hint], or [value] is not null.
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
......@@ -3296,6 +3328,10 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.isSelected = selected;
if (label != null)
config.label = label;
if (value != null)
config.value = value;
if (hint != null)
config.hint = hint;
if (textDirection != null)
config.textDirection = textDirection;
if (button != null)
......
......@@ -4453,6 +4453,8 @@ class Semantics extends SingleChildRenderObjectWidget {
this.selected,
this.button,
this.label,
this.value,
this.hint,
this.textDirection,
}) : assert(container != null),
super(key: key, child: child);
......@@ -4502,15 +4504,40 @@ class Semantics extends SingleChildRenderObjectWidget {
///
/// If a label is provided, there must either by an ambient [Directionality]
/// or an explicit [textDirection] should be provided.
///
/// See also:
/// * [SemanticsConfiguration.label] for a description of how this is exposed
/// in TalkBack and VoiceOver.
final String label;
/// The reading direction of the [label].
/// Provides a textual description of the value of the widget.
///
/// If a value is provided, there must either by an ambient [Directionality]
/// or an explicit [textDirection] should be provided.
///
/// See also:
/// * [SemanticsConfiguration.value] for a description of how this is exposed
/// in TalkBack and VoiceOver.
final String value;
/// Provides a brief textual description of the result of an action performed
/// on the widget.
///
/// If a hint is provided, there must either by an ambient [Directionality]
/// or an explicit [textDirection] should be provided.
///
/// See also:
/// * [SemanticsConfiguration.hint] for a description of how this is exposed
/// in TalkBack and VoiceOver.
final String hint;
/// The reading direction of the [label], [value], and [hint].
///
/// Defaults to the ambient [Directionality].
final TextDirection textDirection;
TextDirection _getTextDirection(BuildContext context) {
return textDirection ?? (label != null ? Directionality.of(context) : null);
return textDirection ?? (label != null || value != null || hint != null ? Directionality.of(context) : null);
}
@override
......@@ -4520,8 +4547,10 @@ class Semantics extends SingleChildRenderObjectWidget {
explicitChildNodes: explicitChildNodes,
checked: checked,
selected: selected,
label: label,
button: button,
label: label,
value: value,
hint: hint,
textDirection: _getTextDirection(context),
);
}
......@@ -4534,6 +4563,8 @@ class Semantics extends SingleChildRenderObjectWidget {
..checked = checked
..selected = selected
..label = label
..value = value
..hint = hint
..textDirection = _getTextDirection(context);
}
......@@ -4544,6 +4575,8 @@ class Semantics extends SingleChildRenderObjectWidget {
description.add(new DiagnosticsProperty<bool>('checked', checked, defaultValue: null));
description.add(new DiagnosticsProperty<bool>('selected', selected, defaultValue: null));
description.add(new StringProperty('label', label, defaultValue: ''));
description.add(new StringProperty('value', value));
description.add(new StringProperty('hint', hint));
description.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
}
}
......
......@@ -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, label: "", isButton: false, textDirection: null)\n',
'SemanticsNode#16(owner: null, isPartOfNodeMerging: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), actions: [], isSelected: false, isButton: false, label: "", value: "", hint: "", textDirection: null)\n',
);
final SemanticsConfiguration config = new SemanticsConfiguration()
......@@ -217,7 +217,7 @@ void main() {
..updateWith(config: config, childrenInInversePaintOrder: null);
expect(
allProperties.toStringDeep(),
'SemanticsNode#17(STALE, owner: null, leaf merge, Rect.fromLTRB(60.0, 20.0, 80.0, 50.0), actions: [longPress, scrollUp, showOnScreen], unchecked, selected, label: "Use all the properties", button, textDirection: rtl)\n',
'SemanticsNode#17(STALE, owner: null, leaf merge, Rect.fromLTRB(60.0, 20.0, 80.0, 50.0), actions: [longPress, scrollUp, showOnScreen], unchecked, selected, button, label: "Use all the properties", textDirection: rtl)\n',
);
expect(
allProperties.getSemanticsData().toString(),
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -220,4 +221,153 @@ void main() {
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
});
testWidgets('Semantics label and hint', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Semantics(
label: 'label',
hint: 'hint',
value: 'value',
child: new Container(),
),
),
);
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
label: 'label',
hint: 'hint',
value: 'value',
textDirection: TextDirection.ltr,
)
]
);
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
});
testWidgets('Semantics hints can merge', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Semantics(
container: true,
child: new Column(
children: <Widget>[
const Semantics(
hint: 'hint one',
),
const Semantics(
hint: 'hint two',
)
],
),
),
),
);
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
hint: 'hint one\nhint two',
textDirection: TextDirection.ltr,
)
]
);
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
});
testWidgets('Semantics values do not merge', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Semantics(
container: true,
child: new Column(
children: <Widget>[
new Semantics(
value: 'value one',
child: new Container(
height: 10.0,
width: 10.0,
)
),
new Semantics(
value: 'value two',
child: new Container(
height: 10.0,
width: 10.0,
)
)
],
),
),
),
);
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
children: <TestSemantics>[
new TestSemantics(
value: 'value one',
textDirection: TextDirection.ltr,
),
new TestSemantics(
value: 'value two',
textDirection: TextDirection.ltr,
),
]
)
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
});
testWidgets('Semantics value and hint can merge', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Semantics(
container: true,
child: new Column(
children: <Widget>[
const Semantics(
hint: 'hint',
),
const Semantics(
value: 'value',
),
],
),
),
),
);
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
hint: 'hint',
value: 'value',
textDirection: TextDirection.ltr,
)
]
);
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
});
}
......@@ -35,6 +35,8 @@ class TestSemantics {
this.flags: 0,
this.actions: 0,
this.label: '',
this.value: '',
this.hint: '',
this.textDirection,
this.rect,
this.transform,
......@@ -42,6 +44,8 @@ class TestSemantics {
Iterable<SemanticsTag> tags,
}) : assert(flags != null),
assert(label != null),
assert(value != null),
assert(hint != null),
assert(children != null),
tags = tags?.toSet() ?? new Set<SemanticsTag>();
......@@ -51,6 +55,8 @@ class TestSemantics {
this.flags: 0,
this.actions: 0,
this.label: '',
this.value: '',
this.hint: '',
this.textDirection,
this.transform,
this.children: const <TestSemantics>[],
......@@ -58,6 +64,8 @@ class TestSemantics {
}) : id = 0,
assert(flags != null),
assert(label != null),
assert(value != null),
assert(hint != null),
rect = TestSemantics.rootRect,
assert(children != null),
tags = tags?.toSet() ?? new Set<SemanticsTag>();
......@@ -76,6 +84,8 @@ class TestSemantics {
this.flags: 0,
this.actions: 0,
this.label: '',
this.hint: '',
this.value: '',
this.textDirection,
this.rect,
Matrix4 transform,
......@@ -83,6 +93,8 @@ class TestSemantics {
Iterable<SemanticsTag> tags,
}) : assert(flags != null),
assert(label != null),
assert(value != null),
assert(hint != null),
transform = _applyRootChildScale(transform),
assert(children != null),
tags = tags?.toSet() ?? new Set<SemanticsTag>();
......@@ -102,6 +114,13 @@ class TestSemantics {
/// A textual description of this node.
final String label;
/// A textual description for the value of this node.
final String value;
/// A brief textual description of the result of the action that can be
/// performed on this node.
final String hint;
/// The reading direction of the [label].
///
/// Even if this is not set, the [hasSemantics] matcher will verify that if a
......@@ -169,10 +188,14 @@ class TestSemantics {
return fail('expected node id $id to have actions $actions but found actions ${nodeData.actions}.');
if (label != nodeData.label)
return fail('expected node id $id to have label "$label" but found label "${nodeData.label}".');
if (value != nodeData.value)
return fail('expected node id $id to have value "$value" but found value "${nodeData.value}".');
if (hint != nodeData.hint)
return fail('expected node id $id to have hint "$hint" but found hint "${nodeData.hint}".');
if (textDirection != null && textDirection != nodeData.textDirection)
return fail('expected node id $id to have textDirection "$textDirection" but found "${nodeData.textDirection}".');
if (nodeData.label != '' && nodeData.textDirection == null)
return fail('expected node id $id, which has a label, to have a textDirection, but it did not.');
if ((nodeData.label != '' || nodeData.value != '' || nodeData.hint != '') && nodeData.textDirection == null)
return fail('expected node id $id, which has a label, value, or hint, to have a textDirection, but it did not.');
if (!ignoreRect && rect != nodeData.rect)
return fail('expected node id $id to have rect $rect but found rect ${nodeData.rect}.');
if (!ignoreTransform && transform != nodeData.transform)
......
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