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> {
// then give all of the tabs equal flexibility so that their widths
// reflect the intrinsic width of their labels.
for (int index = 0; index < widget.tabs.length; index++) {
wrappedTabs[index] = new InkWell(
onTap: () { _handleTap(index); },
child: wrappedTabs[index],
wrappedTabs[index] = new MergeSemantics(
child: new Semantics(
selected: index == _currentIndex,
child: new InkWell(
onTap: () { _handleTap(index); },
child: wrappedTabs[index],
),
),
);
if (!widget.isScrollable)
wrappedTabs[index] = new Expanded(child: wrappedTabs[index]);
......
......@@ -2873,10 +2873,12 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
RenderBox child,
bool container: false,
bool checked,
String label
bool selected,
String label,
}) : assert(container != null),
_container = container,
_checked = checked,
_selected = selected,
_label = label,
super(child);
......@@ -2900,8 +2902,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate();
}
/// If non-null, sets the "hasCheckedState" semantic to true and the
/// "isChecked" semantic to the given value.
/// If non-null, sets the [SemanticsNode.hasCheckedState] semantic to true and
/// the [SemanticsNode.isChecked] semantic to the given value.
bool get checked => _checked;
bool _checked;
set checked(bool value) {
......@@ -2912,7 +2914,19 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
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 _label;
set label(String value) {
......@@ -2927,7 +2941,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
bool get isSemanticBoundary => container;
@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) {
if (checked != null) {
......@@ -2935,6 +2949,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
..hasCheckedState = true
..isChecked = checked;
}
if (selected != null)
node.isSelected = selected;
if (label != null)
node.label = label;
}
......
......@@ -292,6 +292,10 @@ class SemanticsNode extends AbstractNode {
bool get isChecked => (_flags & SemanticsFlags.isChecked.index) != 0;
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.
String get label => _label;
String _label = '';
......@@ -595,6 +599,8 @@ class SemanticsNode extends AbstractNode {
else
buffer.write('; unchecked');
}
if (isSelected)
buffer.write('; selected');
if (label.isNotEmpty)
buffer.write('; "$label"');
buffer.write(')');
......
......@@ -3635,7 +3635,8 @@ class Semantics extends SingleChildRenderObjectWidget {
Widget child,
this.container: false,
this.checked,
this.label
this.selected,
this.label,
}) : assert(container != null),
super(key: key, child: child);
......@@ -3656,6 +3657,13 @@ class Semantics extends SingleChildRenderObjectWidget {
/// state is.
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.
final String label;
......@@ -3663,7 +3671,8 @@ class Semantics extends SingleChildRenderObjectWidget {
RenderSemanticsAnnotations createRenderObject(BuildContext context) => new RenderSemanticsAnnotations(
container: container,
checked: checked,
label: label
selected: selected,
label: label,
);
@override
......@@ -3671,6 +3680,7 @@ class Semantics extends SingleChildRenderObjectWidget {
renderObject
..container = container
..checked = checked
..selected = selected
..label = label;
}
......@@ -3680,6 +3690,8 @@ class Semantics extends SingleChildRenderObjectWidget {
description.add('container: $container');
if (checked != null)
description.add('checked: $checked');
if (selected != null)
description.add('selected: $selected');
if (label != null)
description.add('label: "$label"');
}
......
......@@ -2,12 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show SemanticsFlags, SemanticsAction;
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import '../rendering/mock_canvas.dart';
import '../rendering/recording_canvas.dart';
import '../widgets/semantics_tester.dart';
class StateMarker extends StatefulWidget {
const StateMarker({ Key key, this.child }) : super(key: key);
......@@ -900,6 +903,62 @@ void main() {
rect: new Rect.fromLTRB(tabLeft + padLeft, height, tabRight - padRight, height + weight)
));
});
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 {
final TabController controller = new TabController(
......
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