Commit 0774c519 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Add SemanticsNode.isSelected flag (#10610)

* Add SemanticsNode.isSelected flag

* Adds example usage to TabBar

See also https://github.com/flutter/engine/pull/3764

* Review comments

* whitespace fixes

* Fix doc ref and update engine roll
parent 1f4f75bb
...@@ -730,9 +730,14 @@ class _TabBarState extends State<TabBar> { ...@@ -730,9 +730,14 @@ class _TabBarState extends State<TabBar> {
// then give all of the tabs equal flexibility so that their widths // then give all of the tabs equal flexibility so that their widths
// reflect the intrinsic width of their labels. // reflect the intrinsic width of their labels.
for (int index = 0; index < widget.tabs.length; index++) { for (int index = 0; index < widget.tabs.length; index++) {
wrappedTabs[index] = new InkWell( wrappedTabs[index] = new MergeSemantics(
child: new Semantics(
selected: index == _currentIndex,
child: new InkWell(
onTap: () { _handleTap(index); }, onTap: () { _handleTap(index); },
child: wrappedTabs[index], child: wrappedTabs[index],
),
),
); );
if (!widget.isScrollable) if (!widget.isScrollable)
wrappedTabs[index] = new Expanded(child: wrappedTabs[index]); wrappedTabs[index] = new Expanded(child: wrappedTabs[index]);
......
...@@ -2873,10 +2873,12 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -2873,10 +2873,12 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
RenderBox child, RenderBox child,
bool container: false, bool container: false,
bool checked, bool checked,
String label bool selected,
String label,
}) : assert(container != null), }) : assert(container != null),
_container = container, _container = container,
_checked = checked, _checked = checked,
_selected = selected,
_label = label, _label = label,
super(child); super(child);
...@@ -2900,8 +2902,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -2900,8 +2902,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
/// If non-null, sets the "hasCheckedState" semantic to true and the /// If non-null, sets the [SemanticsNode.hasCheckedState] semantic to true and
/// "isChecked" semantic to the given value. /// the [SemanticsNode.isChecked] semantic to the given value.
bool get checked => _checked; bool get checked => _checked;
bool _checked; bool _checked;
set checked(bool value) { set checked(bool value) {
...@@ -2912,7 +2914,19 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -2912,7 +2914,19 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate(onlyChanges: (value != null) == hadValue); markNeedsSemanticsUpdate(onlyChanges: (value != null) == hadValue);
} }
/// If non-null, sets the "label" semantic to the given value. /// If non-null, sets the [SemanticsNode.isSelected] semantic to the given
/// value.
bool get selected => _selected;
bool _selected;
set selected(bool value) {
if (selected == value)
return;
final bool hadValue = selected != null;
_selected = value;
markNeedsSemanticsUpdate(onlyChanges: (value != null) == hadValue);
}
/// If non-null, sets the [SemanticsNode.label] semantic to the given value.
String get label => _label; String get label => _label;
String _label; String _label;
set label(String value) { set label(String value) {
...@@ -2927,7 +2941,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -2927,7 +2941,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
bool get isSemanticBoundary => container; bool get isSemanticBoundary => container;
@override @override
SemanticsAnnotator get semanticsAnnotator => checked != null || label != null ? _annotate : null; SemanticsAnnotator get semanticsAnnotator => checked != null || selected != null || label != null ? _annotate : null;
void _annotate(SemanticsNode node) { void _annotate(SemanticsNode node) {
if (checked != null) { if (checked != null) {
...@@ -2935,6 +2949,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -2935,6 +2949,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
..hasCheckedState = true ..hasCheckedState = true
..isChecked = checked; ..isChecked = checked;
} }
if (selected != null)
node.isSelected = selected;
if (label != null) if (label != null)
node.label = label; node.label = label;
} }
......
...@@ -292,6 +292,10 @@ class SemanticsNode extends AbstractNode { ...@@ -292,6 +292,10 @@ class SemanticsNode extends AbstractNode {
bool get isChecked => (_flags & SemanticsFlags.isChecked.index) != 0; bool get isChecked => (_flags & SemanticsFlags.isChecked.index) != 0;
set isChecked(bool value) => _setFlag(SemanticsFlags.isChecked, value); set isChecked(bool value) => _setFlag(SemanticsFlags.isChecked, value);
/// Whether the current node is selected (true) or not (false).
bool get isSelected => (_flags & SemanticsFlags.isSelected.index) != 0;
set isSelected(bool value) => _setFlag(SemanticsFlags.isSelected, value);
/// A textual description of this node. /// A textual description of this node.
String get label => _label; String get label => _label;
String _label = ''; String _label = '';
...@@ -595,6 +599,8 @@ class SemanticsNode extends AbstractNode { ...@@ -595,6 +599,8 @@ class SemanticsNode extends AbstractNode {
else else
buffer.write('; unchecked'); buffer.write('; unchecked');
} }
if (isSelected)
buffer.write('; selected');
if (label.isNotEmpty) if (label.isNotEmpty)
buffer.write('; "$label"'); buffer.write('; "$label"');
buffer.write(')'); buffer.write(')');
......
...@@ -3635,7 +3635,8 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -3635,7 +3635,8 @@ class Semantics extends SingleChildRenderObjectWidget {
Widget child, Widget child,
this.container: false, this.container: false,
this.checked, this.checked,
this.label this.selected,
this.label,
}) : assert(container != null), }) : assert(container != null),
super(key: key, child: child); super(key: key, child: child);
...@@ -3656,6 +3657,13 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -3656,6 +3657,13 @@ class Semantics extends SingleChildRenderObjectWidget {
/// state is. /// state is.
final bool checked; final bool checked;
/// If non-null indicates that this subtree represents something that can be
/// in a selected or unselected state, and what its current state is.
///
/// The active tab in a tab bar for example is considered "selected", whereas
/// all other tabs are unselected.
final bool selected;
/// Provides a textual description of the widget. /// Provides a textual description of the widget.
final String label; final String label;
...@@ -3663,7 +3671,8 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -3663,7 +3671,8 @@ class Semantics extends SingleChildRenderObjectWidget {
RenderSemanticsAnnotations createRenderObject(BuildContext context) => new RenderSemanticsAnnotations( RenderSemanticsAnnotations createRenderObject(BuildContext context) => new RenderSemanticsAnnotations(
container: container, container: container,
checked: checked, checked: checked,
label: label selected: selected,
label: label,
); );
@override @override
...@@ -3671,6 +3680,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -3671,6 +3680,7 @@ class Semantics extends SingleChildRenderObjectWidget {
renderObject renderObject
..container = container ..container = container
..checked = checked ..checked = checked
..selected = selected
..label = label; ..label = label;
} }
...@@ -3680,6 +3690,8 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -3680,6 +3690,8 @@ class Semantics extends SingleChildRenderObjectWidget {
description.add('container: $container'); description.add('container: $container');
if (checked != null) if (checked != null)
description.add('checked: $checked'); description.add('checked: $checked');
if (selected != null)
description.add('selected: $selected');
if (label != null) if (label != null)
description.add('label: "$label"'); description.add('label: "$label"');
} }
......
...@@ -2,12 +2,15 @@ ...@@ -2,12 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' show SemanticsFlags, SemanticsAction;
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
import '../rendering/recording_canvas.dart'; import '../rendering/recording_canvas.dart';
import '../widgets/semantics_tester.dart';
class StateMarker extends StatefulWidget { class StateMarker extends StatefulWidget {
const StateMarker({ Key key, this.child }) : super(key: key); const StateMarker({ Key key, this.child }) : super(key: key);
...@@ -901,6 +904,62 @@ void main() { ...@@ -901,6 +904,62 @@ void main() {
)); ));
}); });
testWidgets('correct semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
final List<Tab> tabs = new List<Tab>.generate(2, (int index) {
return new Tab(text: 'TAB #$index');
});
final TabController controller = new TabController(
vsync: const TestVSync(),
length: tabs.length,
initialIndex: 0,
);
await tester.pumpWidget(
new Material(
child: new Semantics(
container: true,
child: new TabBar(
isScrollable: true,
controller: controller,
tabs: tabs,
),
),
),
);
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
new TestSemantics(
id: 2,
actions: SemanticsAction.tap.index,
flags: SemanticsFlags.isSelected.index,
label: 'TAB #0',
rect: new Rect.fromLTRB(0.0, 0.0, 108.0, 46.0),
transform: new Matrix4.translationValues(0.0, 276.0, 0.0),
),
new TestSemantics(
id: 4,
actions: SemanticsAction.tap.index,
label: 'TAB #1',
rect: new Rect.fromLTRB(0.0, 0.0, 108.0, 46.0),
transform: new Matrix4.translationValues(108.0, 276.0, 0.0),
),
]),
],
);
expect(semantics, hasSemantics(expectedSemantics));
semantics.dispose();
});
testWidgets('TabBar etc with zero tabs', (WidgetTester tester) async { testWidgets('TabBar etc with zero tabs', (WidgetTester tester) async {
final TabController controller = new TabController( final TabController controller = new TabController(
vsync: const TestVSync(), vsync: const TestVSync(),
......
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