diff --git a/bin/internal/engine.version b/bin/internal/engine.version index 28b7a84b944..f7c3bfd3fd3 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -8e79156765c67b71b1e1b9895dbc8eea43f9949c +91071f817b2f6a0f6684e1f2fda3d8b21314bcb7 diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 7d210d25642..1eae2eaf91b 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -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,17 +3254,6 @@ class RenderSemanticsAnnotations extends RenderProxyBox { markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue); } - /// If non-null, sets the [SemanticsNode.label] semantic to the given value. - String get label => _label; - String _label; - set label(String value) { - if (label == value) - return; - 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; @@ -3272,9 +3265,48 @@ class RenderSemanticsAnnotations extends RenderProxyBox { 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) + return; + final bool hadValue = _label != null; + _label = value; + markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue); + } + + /// 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 = _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) diff --git a/packages/flutter/lib/src/rendering/semantics.dart b/packages/flutter/lib/src/rendering/semantics.dart index cd0ce69ff06..357827ae398 100644 --- a/packages/flutter/lib/src/rendering/semantics.dart +++ b/packages/flutter/lib/src/rendering/semantics.dart @@ -76,6 +76,8 @@ class SemanticsData extends Diagnosticable { @required this.flags, @required this.actions, @required this.label, + @required this.value, + @required this.hint, @required this.textDirection, @required this.rect, this.tags, @@ -97,7 +99,17 @@ class SemanticsData extends Diagnosticable { /// The text's reading direction is given by [textDirection]. final String label; - /// The reading direction for the text in [label]. + /// A textual description for the current value of the node. + /// + /// The text's reading direction is given by [textDirection]. + final String value; + + /// A brief description of the result of performing an action on this node. + /// + /// The text's reading direction is given by [textDirection]. + final String hint; + + /// The reading direction for the text in [label], [value], and [hint]. final TextDirection textDirection; /// The bounding box for this node in its coordinate system. @@ -485,6 +497,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { bool _isDifferentFromCurrentSemanticAnnotation(SemanticsConfiguration config) { return _label != config.label || + _hint != config.hint || + _value != config.value || _flags != config._flags || _textDirection != config.textDirection || _actionsAsBits != config._actionsAsBits || @@ -516,7 +530,19 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { String get label => _label; String _label = _kEmptyConfig.label; - /// The reading direction for [label]. + /// A textual description for the current value of the node. + /// + /// The text's reading direction is given by [textDirection]. + String get value => _value; + String _value = _kEmptyConfig.value; + + /// A brief description of the result of performing an action on this node. + /// + /// The text's reading direction is given by [textDirection]. + String get hint => _hint; + String _hint = _kEmptyConfig.hint; + + /// The reading direction for [label], [value], and [hint]. TextDirection get textDirection => _textDirection; TextDirection _textDirection = _kEmptyConfig.textDirection; @@ -533,6 +559,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { _markDirty(); _label = config.label; + _value = config.value; + _hint = config.hint; _flags = config._flags; _textDirection = config.textDirection; _actions = new Map.from(config._actions); @@ -551,6 +579,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { int flags = _flags; int actions = _actionsAsBits; String label = _label; + String hint = _hint; + String value = _value; TextDirection textDirection = _textDirection; Set mergedTags = tags == null ? null : new Set.from(tags); @@ -560,27 +590,24 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { flags |= node._flags; actions |= node._actionsAsBits; textDirection ??= node._textDirection; + if (value == '' || value == null) + value = node._value; if (node.tags != null) { mergedTags ??= new Set(); mergedTags.addAll(node.tags); } - if (node._label.isNotEmpty) { - String nestedLabel = node._label; - if (textDirection != node._textDirection && node._textDirection != null) { - switch (node._textDirection) { - case TextDirection.rtl: - nestedLabel = '${Unicode.RLE}$nestedLabel${Unicode.PDF}'; - break; - case TextDirection.ltr: - nestedLabel = '${Unicode.LRE}$nestedLabel${Unicode.PDF}'; - break; - } - } - if (label.isEmpty) - label = nestedLabel; - else - label = '$label\n$nestedLabel'; - } + label = _concatStrings( + thisString: label, + thisTextDirection: textDirection, + otherString: node._label, + otherTextDirection: node._textDirection, + ); + hint = _concatStrings( + thisString: hint, + thisTextDirection: textDirection, + otherString: node._hint, + otherTextDirection: node._textDirection, + ); return true; }); } @@ -589,6 +616,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { flags: flags, actions: actions, label: label, + value: value, + hint: hint, textDirection: textDirection, rect: rect, transform: transform, @@ -621,6 +650,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { actions: data.actions, rect: data.rect, label: data.label, + value: data.value, + hint: data.hint, textDirection: data.textDirection, transform: data.transform?.storage ?? _kIdentityTransform, children: children, @@ -676,8 +707,10 @@ 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 StringProperty('label', _label, defaultValue: '')); properties.add(new FlagProperty('isButton', value: _hasFlag(SemanticsFlags.isButton), ifTrue: 'button')); + properties.add(new StringProperty('label', _label, defaultValue: '')); + properties.add(new StringProperty('value', _value, defaultValue: '')); + properties.add(new StringProperty('hint', _hint, defaultValue: '')); properties.add(new EnumProperty('textDirection', _textDirection, defaultValue: null)); } @@ -1015,6 +1048,11 @@ class SemanticsConfiguration { /// A textual description of the owning [RenderObject]. /// + /// On iOS this is used for the `accessibilityLabel` property defined in the + /// `UIAccessibility` Protocol. On Android it is concatenated together with + /// [value] and [hint] in the following order: [value], [label], [hint]. + /// The concatenated value is then used as the `Text` description. + /// /// The text's reading direction is given by [textDirection]. String get label => _label; String _label = ''; @@ -1023,7 +1061,37 @@ class SemanticsConfiguration { _hasBeenAnnotated = true; } - /// The reading direction for the text in [label]. + /// A textual description for the current value of the owning [RenderObject]. + /// + /// On iOS this is used for the `accessibilityValue` property defined in the + /// `UIAccessibility` Protocol. On Android it is concatenated together with + /// [label] and [hint] in the following order: [value], [label], [hint]. + /// The concatenated value is then used as the `Text` description. + /// + /// The text's reading direction is given by [textDirection]. + String get value => _value; + String _value = ''; + set value(String value) { + _value = value; + _hasBeenAnnotated = true; + } + + /// A brief description of the result of performing an action on this node. + /// + /// On iOS this is used for the `accessibilityHint` property defined in the + /// `UIAccessibility` Protocol. On Android it is concatenated together with + /// [label] and [value] in the following order: [value], [label], [hint]. + /// The concatenated value is then used as the `Text` description. + /// + /// The text's reading direction is given by [textDirection]. + String get hint => _hint; + String _hint = ''; + set hint(String hint) { + _hint = hint; + _hasBeenAnnotated = true; + } + + /// The reading direction for the text in [label], [value], and [hint]. TextDirection get textDirection => _textDirection; TextDirection _textDirection; set textDirection(TextDirection textDirection) { @@ -1087,6 +1155,8 @@ class SemanticsConfiguration { return false; if ((_flags & other._flags) != 0) return false; + if (_value != null && _value.isNotEmpty && other._value != null && other._value.isNotEmpty) + return false; return true; } @@ -1109,23 +1179,20 @@ class SemanticsConfiguration { _flags |= other._flags; textDirection ??= other.textDirection; - if (other.label.isNotEmpty) { - String nestedLabel = other.label; - if (textDirection != other.textDirection && other.textDirection != null) { - switch (other.textDirection) { - case TextDirection.rtl: - nestedLabel = '${Unicode.RLE}$nestedLabel${Unicode.PDF}'; - break; - case TextDirection.ltr: - nestedLabel = '${Unicode.LRE}$nestedLabel${Unicode.PDF}'; - break; - } - } - if (label.isEmpty) - label = nestedLabel; - else - label = '$label\n$nestedLabel'; - } + _label = _concatStrings( + thisString: _label, + thisTextDirection: textDirection, + otherString: other._label, + otherTextDirection: other.textDirection, + ); + if (_value == '' || _value == null) + _value = other._value; + _hint = _concatStrings( + thisString: _hint, + thisTextDirection: textDirection, + otherString: other._hint, + otherTextDirection: other.textDirection, + ); _hasBeenAnnotated = _hasBeenAnnotated || other._hasBeenAnnotated; } @@ -1138,8 +1205,34 @@ class SemanticsConfiguration { .._hasBeenAnnotated = _hasBeenAnnotated .._textDirection = _textDirection .._label = _label + .._value = _value + .._hint = _hint .._flags = _flags .._actionsAsBits = _actionsAsBits .._actions.addAll(_actions); } } + +String _concatStrings({ + @required String thisString, + @required String otherString, + @required TextDirection thisTextDirection, + @required TextDirection otherTextDirection +}) { + if (otherString.isEmpty) + return thisString; + String nestedLabel = otherString; + if (thisTextDirection != otherTextDirection && otherTextDirection != null) { + switch (otherTextDirection) { + case TextDirection.rtl: + nestedLabel = '${Unicode.RLE}$nestedLabel${Unicode.PDF}'; + break; + case TextDirection.ltr: + nestedLabel = '${Unicode.LRE}$nestedLabel${Unicode.PDF}'; + break; + } + } + if (thisString.isEmpty) + return nestedLabel; + return '$thisString\n$nestedLabel'; +} diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 9d91bb6e7ba..e0037dba54e 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -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('checked', checked, defaultValue: null)); description.add(new DiagnosticsProperty('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, defaultValue: null)); } } diff --git a/packages/flutter/test/rendering/semantics_test.dart b/packages/flutter/test/rendering/semantics_test.dart index 9f492b00071..e06c6cc8779 100644 --- a/packages/flutter/test/rendering/semantics_test.dart +++ b/packages/flutter/test/rendering/semantics_test.dart @@ -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(), diff --git a/packages/flutter/test/widgets/semantics_test.dart b/packages/flutter/test/widgets/semantics_test.dart index 573239ecd4f..07d4f527194 100644 --- a/packages/flutter/test/widgets/semantics_test.dart +++ b/packages/flutter/test/widgets/semantics_test.dart @@ -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: [ + 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: [ + const Semantics( + hint: 'hint one', + ), + const Semantics( + hint: 'hint two', + ) + + ], + ), + ), + ), + ); + + final TestSemantics expectedSemantics = new TestSemantics.root( + children: [ + 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: [ + 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: [ + new TestSemantics.rootChild( + children: [ + 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: [ + const Semantics( + hint: 'hint', + ), + const Semantics( + value: 'value', + ), + ], + ), + ), + ), + ); + + final TestSemantics expectedSemantics = new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + hint: 'hint', + value: 'value', + textDirection: TextDirection.ltr, + ) + ] + ); + + expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true)); + }); } diff --git a/packages/flutter/test/widgets/semantics_tester.dart b/packages/flutter/test/widgets/semantics_tester.dart index 4bdb3f81598..5656e5561c9 100644 --- a/packages/flutter/test/widgets/semantics_tester.dart +++ b/packages/flutter/test/widgets/semantics_tester.dart @@ -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 tags, }) : assert(flags != null), assert(label != null), + assert(value != null), + assert(hint != null), assert(children != null), tags = tags?.toSet() ?? new Set(); @@ -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 [], @@ -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(); @@ -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 tags, }) : assert(flags != null), assert(label != null), + assert(value != null), + assert(hint != null), transform = _applyRootChildScale(transform), assert(children != null), tags = tags?.toSet() ?? new Set(); @@ -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)