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 { ...@@ -3176,6 +3176,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
bool selected, bool selected,
bool button, bool button,
String label, String label,
String value,
String hint,
TextDirection textDirection, TextDirection textDirection,
}) : assert(container != null), }) : assert(container != null),
_container = container, _container = container,
...@@ -3184,6 +3186,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3184,6 +3186,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_selected = selected, _selected = selected,
_button = button, _button = button,
_label = label, _label = label,
_value = value,
_hint = hint,
_textDirection = textDirection, _textDirection = textDirection,
super(child); super(child);
...@@ -3250,31 +3254,59 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3250,31 +3254,59 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue); 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. /// 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 get label => _label;
String _label; String _label;
set label(String value) { set label(String value) {
if (label == value) if (_label == value)
return; return;
final bool hadValue = label != null; final bool hadValue = _label != null;
_label = value; _label = value;
markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue); markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue);
} }
/// If non-null, sets the [SemanticsNode.isButton] semantic to the given value. /// If non-null, sets the [SemanticsNode.value] semantic to the given value.
bool get button => _button; ///
bool _button; /// The text's reading direction is given by [textDirection].
set button(bool value) { String get value => _value;
if (button == value) String _value;
set value(String value) {
if (_value == value)
return; return;
final bool hadValue = button != null; final bool hadValue = _value != null;
_button = value; _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); markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue);
} }
/// If non-null, sets the [SemanticsNode.textDirection] semantic to the given value. /// 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 get textDirection => _textDirection;
TextDirection _textDirection; TextDirection _textDirection;
set textDirection(TextDirection value) { set textDirection(TextDirection value) {
...@@ -3296,6 +3328,10 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3296,6 +3328,10 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.isSelected = selected; config.isSelected = selected;
if (label != null) if (label != null)
config.label = label; config.label = label;
if (value != null)
config.value = value;
if (hint != null)
config.hint = hint;
if (textDirection != null) if (textDirection != null)
config.textDirection = textDirection; config.textDirection = textDirection;
if (button != null) if (button != null)
......
...@@ -4453,6 +4453,8 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4453,6 +4453,8 @@ class Semantics extends SingleChildRenderObjectWidget {
this.selected, this.selected,
this.button, this.button,
this.label, this.label,
this.value,
this.hint,
this.textDirection, this.textDirection,
}) : assert(container != null), }) : assert(container != null),
super(key: key, child: child); super(key: key, child: child);
...@@ -4502,15 +4504,40 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4502,15 +4504,40 @@ class Semantics extends SingleChildRenderObjectWidget {
/// ///
/// If a label is provided, there must either by an ambient [Directionality] /// If a label is provided, there must either by an ambient [Directionality]
/// or an explicit [textDirection] should be provided. /// 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; 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]. /// Defaults to the ambient [Directionality].
final TextDirection textDirection; final TextDirection textDirection;
TextDirection _getTextDirection(BuildContext context) { 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 @override
...@@ -4520,8 +4547,10 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4520,8 +4547,10 @@ class Semantics extends SingleChildRenderObjectWidget {
explicitChildNodes: explicitChildNodes, explicitChildNodes: explicitChildNodes,
checked: checked, checked: checked,
selected: selected, selected: selected,
label: label,
button: button, button: button,
label: label,
value: value,
hint: hint,
textDirection: _getTextDirection(context), textDirection: _getTextDirection(context),
); );
} }
...@@ -4534,6 +4563,8 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4534,6 +4563,8 @@ class Semantics extends SingleChildRenderObjectWidget {
..checked = checked ..checked = checked
..selected = selected ..selected = selected
..label = label ..label = label
..value = value
..hint = hint
..textDirection = _getTextDirection(context); ..textDirection = _getTextDirection(context);
} }
...@@ -4544,6 +4575,8 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4544,6 +4575,8 @@ class Semantics extends SingleChildRenderObjectWidget {
description.add(new DiagnosticsProperty<bool>('checked', checked, defaultValue: null)); description.add(new DiagnosticsProperty<bool>('checked', checked, defaultValue: null));
description.add(new DiagnosticsProperty<bool>('selected', selected, defaultValue: null)); description.add(new DiagnosticsProperty<bool>('selected', selected, defaultValue: null));
description.add(new StringProperty('label', label, defaultValue: '')); 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)); description.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
} }
} }
......
...@@ -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, 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() final SemanticsConfiguration config = new SemanticsConfiguration()
...@@ -217,7 +217,7 @@ void main() { ...@@ -217,7 +217,7 @@ void main() {
..updateWith(config: config, childrenInInversePaintOrder: null); ..updateWith(config: config, childrenInInversePaintOrder: null);
expect( expect(
allProperties.toStringDeep(), 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( expect(
allProperties.getSemanticsData().toString(), allProperties.getSemanticsData().toString(),
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -220,4 +221,153 @@ void main() { ...@@ -220,4 +221,153 @@ void main() {
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true)); 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 { ...@@ -35,6 +35,8 @@ class TestSemantics {
this.flags: 0, this.flags: 0,
this.actions: 0, this.actions: 0,
this.label: '', this.label: '',
this.value: '',
this.hint: '',
this.textDirection, this.textDirection,
this.rect, this.rect,
this.transform, this.transform,
...@@ -42,6 +44,8 @@ class TestSemantics { ...@@ -42,6 +44,8 @@ class TestSemantics {
Iterable<SemanticsTag> tags, Iterable<SemanticsTag> tags,
}) : assert(flags != null), }) : assert(flags != null),
assert(label != null), assert(label != null),
assert(value != null),
assert(hint != null),
assert(children != null), assert(children != null),
tags = tags?.toSet() ?? new Set<SemanticsTag>(); tags = tags?.toSet() ?? new Set<SemanticsTag>();
...@@ -51,6 +55,8 @@ class TestSemantics { ...@@ -51,6 +55,8 @@ class TestSemantics {
this.flags: 0, this.flags: 0,
this.actions: 0, this.actions: 0,
this.label: '', this.label: '',
this.value: '',
this.hint: '',
this.textDirection, this.textDirection,
this.transform, this.transform,
this.children: const <TestSemantics>[], this.children: const <TestSemantics>[],
...@@ -58,6 +64,8 @@ class TestSemantics { ...@@ -58,6 +64,8 @@ class TestSemantics {
}) : id = 0, }) : id = 0,
assert(flags != null), assert(flags != null),
assert(label != null), assert(label != null),
assert(value != null),
assert(hint != null),
rect = TestSemantics.rootRect, rect = TestSemantics.rootRect,
assert(children != null), assert(children != null),
tags = tags?.toSet() ?? new Set<SemanticsTag>(); tags = tags?.toSet() ?? new Set<SemanticsTag>();
...@@ -76,6 +84,8 @@ class TestSemantics { ...@@ -76,6 +84,8 @@ class TestSemantics {
this.flags: 0, this.flags: 0,
this.actions: 0, this.actions: 0,
this.label: '', this.label: '',
this.hint: '',
this.value: '',
this.textDirection, this.textDirection,
this.rect, this.rect,
Matrix4 transform, Matrix4 transform,
...@@ -83,6 +93,8 @@ class TestSemantics { ...@@ -83,6 +93,8 @@ class TestSemantics {
Iterable<SemanticsTag> tags, Iterable<SemanticsTag> tags,
}) : assert(flags != null), }) : assert(flags != null),
assert(label != null), assert(label != null),
assert(value != null),
assert(hint != null),
transform = _applyRootChildScale(transform), transform = _applyRootChildScale(transform),
assert(children != null), assert(children != null),
tags = tags?.toSet() ?? new Set<SemanticsTag>(); tags = tags?.toSet() ?? new Set<SemanticsTag>();
...@@ -102,6 +114,13 @@ class TestSemantics { ...@@ -102,6 +114,13 @@ class TestSemantics {
/// A textual description of this node. /// A textual description of this node.
final String label; 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]. /// The reading direction of the [label].
/// ///
/// Even if this is not set, the [hasSemantics] matcher will verify that if a /// Even if this is not set, the [hasSemantics] matcher will verify that if a
...@@ -169,10 +188,14 @@ class TestSemantics { ...@@ -169,10 +188,14 @@ class TestSemantics {
return fail('expected node id $id to have actions $actions but found actions ${nodeData.actions}.'); return fail('expected node id $id to have actions $actions but found actions ${nodeData.actions}.');
if (label != nodeData.label) if (label != nodeData.label)
return fail('expected node id $id to have label "$label" but found 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) if (textDirection != null && textDirection != nodeData.textDirection)
return fail('expected node id $id to have textDirection "$textDirection" but found "${nodeData.textDirection}".'); return fail('expected node id $id to have textDirection "$textDirection" but found "${nodeData.textDirection}".');
if (nodeData.label != '' && nodeData.textDirection == null) if ((nodeData.label != '' || nodeData.value != '' || nodeData.hint != '') && nodeData.textDirection == null)
return fail('expected node id $id, which has a label, to have a textDirection, but it did not.'); 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) if (!ignoreRect && rect != nodeData.rect)
return fail('expected node id $id to have rect $rect but found rect ${nodeData.rect}.'); return fail('expected node id $id to have rect $rect but found rect ${nodeData.rect}.');
if (!ignoreTransform && transform != nodeData.transform) 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