diff --git a/examples/flutter_gallery/lib/gallery/home.dart b/examples/flutter_gallery/lib/gallery/home.dart index 694b89be922..cfc8372018e 100644 --- a/examples/flutter_gallery/lib/gallery/home.dart +++ b/examples/flutter_gallery/lib/gallery/home.dart @@ -152,7 +152,10 @@ class GalleryHomeState extends State with SingleTickerProviderState child: new SafeArea( top: false, bottom: false, - child: new Text(galleryItem.category, style: headerStyle), + child: new Semantics( + header: true, + child: new Text(galleryItem.category, style: headerStyle), + ), ), ), ) diff --git a/packages/flutter/lib/src/rendering/custom_paint.dart b/packages/flutter/lib/src/rendering/custom_paint.dart index 79beea4520b..a6dc53b0f4a 100644 --- a/packages/flutter/lib/src/rendering/custom_paint.dart +++ b/packages/flutter/lib/src/rendering/custom_paint.dart @@ -817,6 +817,21 @@ class RenderCustomPaint extends RenderProxyBox { if (properties.button != null) { config.isButton = properties.button; } + if (properties.textField != null) { + config.isTextField = properties.textField; + } + if (properties.focused != null) { + config.isFocused = properties.focused; + } + if (properties.enabled != null) { + config.isEnabled = properties.enabled; + } + if (properties.inMutuallyExclusiveGroup != null) { + config.isInMutuallyExclusiveGroup = properties.inMutuallyExclusiveGroup; + } + if (properties.header != null) { + config.isHeader = properties.header; + } if (properties.label != null) { config.label = properties.label; } diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index a4d994692b1..fc685b608de 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -3015,6 +3015,10 @@ class RenderSemanticsAnnotations extends RenderProxyBox { bool checked, bool selected, bool button, + bool header, + bool textField, + bool focused, + bool inMutuallyExclusiveGroup, String label, String value, String increasedValue, @@ -3045,6 +3049,10 @@ class RenderSemanticsAnnotations extends RenderProxyBox { _checked = checked, _selected = selected, _button = button, + _header = header, + _textField = textField, + _focused = focused, + _inMutuallyExclusiveGroup = inMutuallyExclusiveGroup, _label = label, _value = value, _increasedValue = increasedValue, @@ -3152,6 +3160,47 @@ class RenderSemanticsAnnotations extends RenderProxyBox { markNeedsSemanticsUpdate(); } + /// If non-null, sets the [SemanticsNode.isHeader] semantic to the given value. + bool get header => _header; + bool _header; + set header(bool value) { + if (header == value) + return; + _header = value; + markNeedsSemanticsUpdate(); + } + + /// If non-null, sets the [SemanticsNode.isTextField] semantic to the given value. + bool get textField => _textField; + bool _textField; + set textField(bool value) { + if (textField == value) + return; + _textField = value; + markNeedsSemanticsUpdate(); + } + + /// If non-null, sets the [SemanticsNode.isFocused] semantic to the given value. + bool get focused => _focused; + bool _focused; + set focused(bool value) { + if (focused == value) + return; + _focused = value; + markNeedsSemanticsUpdate(); + } + + /// If non-null, sets the [SemanticsNode.isInMutuallyExclusiveGroup] semantic + /// to the given value. + bool get inMutuallyExclusiveGroup => _inMutuallyExclusiveGroup; + bool _inMutuallyExclusiveGroup; + set inMutuallyExclusiveGroup(bool value) { + if (inMutuallyExclusiveGroup == value) + return; + _inMutuallyExclusiveGroup = value; + markNeedsSemanticsUpdate(); + } + /// If non-null, sets the [SemanticsNode.label] semantic to the given value. /// /// The reading direction is given by [textDirection]. @@ -3581,6 +3630,14 @@ class RenderSemanticsAnnotations extends RenderProxyBox { config.isSelected = selected; if (button != null) config.isButton = button; + if (header != null) + config.isHeader = header; + if (textField != null) + config.isTextField = textField; + if (focused != null) + config.isFocused = focused; + if (inMutuallyExclusiveGroup != null) + config.isInMutuallyExclusiveGroup = inMutuallyExclusiveGroup; if (label != null) config.label = label; if (value != null) diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index 2318fdd3ccf..812c76975ef 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -315,6 +315,10 @@ class SemanticsProperties extends DiagnosticableTree { this.checked, this.selected, this.button, + this.header, + this.textField, + this.focused, + this.inMutuallyExclusiveGroup, this.label, this.value, this.increasedValue, @@ -366,6 +370,35 @@ class SemanticsProperties extends DiagnosticableTree { /// is focused. final bool button; + /// If non-null, indicates that this subtree represents a header. + /// + /// A header divides into sections. For example, an address book application + /// might define headers A, B, C, etc. to divide the list of alphabetically + /// sorted contacts into sections. + final bool header; + + /// If non-null, indicates that this subtree represents a text field. + /// + /// TalkBack/VoiceOver provide special affordances to enter text into a + /// text field. + final bool textField; + + /// If non-null, whether the node currently holds input focus. + /// + /// At most one node in the tree should hold input focus at any point in time. + /// + /// Input focus (indicates that the node will receive keyboard events) is not + /// to be confused with accessibility focus. Accessibility focus is the + /// green/black rectangular that TalkBack/VoiceOver on the screen and is + /// separate from input focus. + final bool focused; + + /// If non-null, whether a semantic node is in a mutually exclusive group. + /// + /// For example, a radio button is in a mutually exclusive group because only + /// one radio button in that group can be marked as [checked]. + final bool inMutuallyExclusiveGroup; + /// Provides a textual description of the widget. /// /// If a label is provided, there must either by an ambient [Directionality] @@ -2365,6 +2398,12 @@ class SemanticsConfiguration { _setFlag(SemanticsFlag.isButton, value); } + /// Whether the owning [RenderObject] is a header (true) or not (false). + bool get isHeader => _hasFlag(SemanticsFlag.isHeader); + set isHeader(bool value) { + _setFlag(SemanticsFlag.isHeader, value); + } + /// Whether the owning [RenderObject] is a text field. bool get isTextField => _hasFlag(SemanticsFlag.isTextField); set isTextField(bool value) { diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 7f1b5653d08..a7cad3a8dc4 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -4847,6 +4847,10 @@ class Semantics extends SingleChildRenderObjectWidget { bool checked, bool selected, bool button, + bool header, + bool textField, + bool focused, + bool inMutuallyExclusiveGroup, String label, String value, String increasedValue, @@ -4881,6 +4885,10 @@ class Semantics extends SingleChildRenderObjectWidget { checked: checked, selected: selected, button: button, + header: header, + textField: textField, + focused: focused, + inMutuallyExclusiveGroup: inMutuallyExclusiveGroup, label: label, value: value, increasedValue: increasedValue, @@ -4960,6 +4968,10 @@ class Semantics extends SingleChildRenderObjectWidget { checked: properties.checked, selected: properties.selected, button: properties.button, + header: properties.header, + textField: properties.textField, + focused: properties.focused, + inMutuallyExclusiveGroup: properties.inMutuallyExclusiveGroup, label: properties.label, value: properties.value, increasedValue: properties.increasedValue, diff --git a/packages/flutter/test/widgets/custom_painter_test.dart b/packages/flutter/test/widgets/custom_painter_test.dart index 8484293430b..04d6fa303d5 100644 --- a/packages/flutter/test/widgets/custom_painter_test.dart +++ b/packages/flutter/test/widgets/custom_painter_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/rendering.dart'; @@ -397,6 +398,52 @@ void _defineTests() { semantics.dispose(); }); + testWidgets('Supports all flags', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await tester.pumpWidget(new CustomPaint( + painter: new _PainterWithSemantics( + semantics: new CustomPainterSemantics( + key: const ValueKey(1), + rect: new Rect.fromLTRB(1.0, 2.0, 3.0, 4.0), + properties: const SemanticsProperties( + enabled: true, + checked: true, + selected: true, + button: true, + textField: true, + focused: true, + inMutuallyExclusiveGroup: true, + header: true, + ), + ), + ), + )); + + const int expectedId = 2; + final TestSemantics expectedSemantics = new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + previousNodeId: -1, + nextNodeId: expectedId, + children: [ + new TestSemantics.rootChild( + id: expectedId, + rect: TestSemantics.fullScreen, + flags: SemanticsFlag.values.values.toList(), + previousNodeId: 1, + nextNodeId: -1, + ), + ] + ), + ], + ); + expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true)); + + semantics.dispose(); + }); + group('diffing', () { testWidgets('complains about duplicate keys', (WidgetTester tester) async { final SemanticsTester semanticsTester = new SemanticsTester(tester); diff --git a/packages/flutter/test/widgets/semantics_test.dart b/packages/flutter/test/widgets/semantics_test.dart index ae925ceda12..d6ad7cba6c1 100644 --- a/packages/flutter/test/widgets/semantics_test.dart +++ b/packages/flutter/test/widgets/semantics_test.dart @@ -449,6 +449,39 @@ void main() { semantics.dispose(); }); + testWidgets('Semantics widget supports all flags', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await tester.pumpWidget( + new Semantics( + container: true, + // flags + enabled: true, + checked: true, + selected: true, + button: true, + textField: true, + focused: true, + inMutuallyExclusiveGroup: true, + header: true, + ) + ); + + final TestSemantics expectedSemantics = new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + rect: TestSemantics.fullScreen, + flags: SemanticsFlag.values.values.toList(), + previousNodeId: -1, + nextNodeId: -1, + ), + ], + ); + expect(semantics, hasSemantics(expectedSemantics, ignoreId: true)); + + semantics.dispose(); + }); + testWidgets('Actions can be replaced without triggering semantics update', (WidgetTester tester) async { final SemanticsTester semantics = new SemanticsTester(tester); int semanticsUpdateCount = 0;