Commit 01694ab6 authored by Jasper van Riet's avatar Jasper van Riet Committed by Hans Muller

Allow detection of taps on TabBar (#23919)

parent a37099f3
......@@ -29,4 +29,5 @@ Lukasz Piliszczuk <lukasz@intheloup.io>
Felix Schmidt <felix.free@gmx.de>
Artur Rymarz <artur.rymarz@gmail.com>
Stefan Mitev <mr.mitew@gmail.com>
Jasper van Riet <jaspervanriet@gmail.com>
Mattijs Fuijkschot <mattijs.fuijkschot@gmail.com>
......@@ -548,6 +548,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.labelPadding,
this.unselectedLabelColor,
this.unselectedLabelStyle,
this.onTap,
}) : assert(tabs != null),
assert(isScrollable != null),
assert(indicator != null || (indicatorWeight != null && indicatorWeight > 0.0)),
......@@ -660,6 +661,17 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// is null then the text style of the theme's body2 definition is used.
final TextStyle unselectedLabelStyle;
/// An optional callback that's called when the [TabBar] is tapped.
///
/// The callback is applied to the index of the tab where the tap occurred.
///
/// This callback has no effect on the default handling of taps. It's for
/// applications that want to do a little extra work when a tab is tapped,
/// even if the tap doesn't change the TabController's index. TabBar [onTap]
/// callbacks should not make changes to the TabController since that would
/// interfere with the default tap handler.
final ValueChanged<int> onTap;
/// A size whose height depends on if the tabs have both icons and text.
///
/// [AppBar] uses this this size to compute its own preferred size.
......@@ -883,6 +895,9 @@ class _TabBarState extends State<TabBar> {
void _handleTap(int index) {
assert(index >= 0 && index < widget.tabs.length);
_controller.animateTo(index);
if (widget.onTap != null) {
widget.onTap(index);
}
}
Widget _buildStyledTab(Widget child, bool selected, Animation<double> animation) {
......
......@@ -1760,6 +1760,87 @@ void main() {
semantics.dispose();
});
testWidgets('can be notified of TabBar onTap behavior', (WidgetTester tester) async {
int tabIndex = -1;
Widget buildFrame({
TabController controller,
List<String> tabs,
}) {
return boilerplate(
child: Container(
child: TabBar(
controller: controller,
tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(),
onTap: (int index) {
tabIndex = index;
},
),
),
);
}
final List<String> tabs = <String>['A', 'B', 'C'];
final TabController controller = TabController(
vsync: const TestVSync(),
length: tabs.length,
initialIndex: tabs.indexOf('C'),
);
await tester.pumpWidget(buildFrame(tabs: tabs, controller: controller));
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsOneWidget);
expect(find.text('C'), findsOneWidget);
expect(controller, isNotNull);
expect(controller.index, 2);
expect(tabIndex, -1); // no tap so far so tabIndex should reflect that
// Verify whether the [onTap] notification works when the [TabBar] animates.
await tester.pumpWidget(buildFrame(tabs: tabs, controller: controller));
await tester.tap(find.text('B'));
await tester.pump();
expect(controller.indexIsChanging, true);
await tester.pumpAndSettle();
expect(controller.index, 1);
expect(controller.previousIndex, 2);
expect(controller.indexIsChanging, false);
expect(tabIndex, controller.index);
tabIndex = -1;
await tester.pumpWidget(buildFrame(tabs: tabs, controller: controller));
await tester.tap(find.text('C'));
await tester.pump();
await tester.pumpAndSettle();
expect(controller.index, 2);
expect(controller.previousIndex, 1);
expect(tabIndex, controller.index);
tabIndex = -1;
await tester.pumpWidget(buildFrame(tabs: tabs, controller: controller));
await tester.tap(find.text('A'));
await tester.pump();
await tester.pumpAndSettle();
expect(controller.index, 0);
expect(controller.previousIndex, 2);
expect(tabIndex, controller.index);
tabIndex = -1;
// Verify whether [onTap] is called even when the [TabController] does
// not change.
final int currentControllerIndex = controller.index;
await tester.pumpWidget(buildFrame(tabs: tabs, controller: controller));
await tester.tap(find.text('A'));
await tester.pump();
await tester.pumpAndSettle();
expect(controller.index, currentControllerIndex); // controller has not changed
expect(tabIndex, 0);
});
test('illegal constructor combinations', () {
expect(() => Tab(icon: nonconst(null)), throwsAssertionError);
expect(() => Tab(icon: Container(), text: 'foo', child: Container()), throwsAssertionError);
......
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