mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add onHover and onFocusChange callbacks for TabBar (#164816)
Fixes https://github.com/flutter/flutter/issues/159444 Fixes https://github.com/flutter/flutter/issues/146089 This adds callbacks to TabBar for onHover and onFocusChange. They pipe through to the underlying Inkwell widget that is wrapped around each Tab of the TabBar during build. #### Alternatives - I did consider adding these callbacks to Tab instead, but felt that going through TabBar would be better. If implemented in Tab, the user would need to define callbacks for each tab. This PR makes it so there is only need for one callback, and the associated Tab index is provided. Also, since the Inkwell is applied in the TabBar, it's kludgy to have to extract that from the Tabs _in_ TabBar later to pass on to the Inkwell. 👃 - Digging in to the requests in the linked issues, the user wants to change various stylings in response to these events. WidgetStateProperties were considered, but there are so many potential styling properties that going this route would require greatly increasing the API surface here. Tab.child allows the user to provide whatever widget they would like to have as the content. Being able to modify Tab.child in response to these events is a better way go instead of exposing a ton of different properties. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
parent
2a866a5757
commit
df676dc6e7
64
examples/api/lib/material/tabs/tab_bar.onFocusChange.dart
Normal file
64
examples/api/lib/material/tabs/tab_bar.onFocusChange.dart
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Flutter code sample for [TabBar.onFocusChange].
|
||||
|
||||
void main() => runApp(const TabBarApp());
|
||||
|
||||
class TabBarApp extends StatelessWidget {
|
||||
const TabBarApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(theme: ThemeData(useMaterial3: true), home: const TabBarExample());
|
||||
}
|
||||
}
|
||||
|
||||
class TabBarExample extends StatefulWidget {
|
||||
const TabBarExample({super.key});
|
||||
|
||||
@override
|
||||
State<TabBarExample> createState() => _TabBarExampleState();
|
||||
}
|
||||
|
||||
class _TabBarExampleState extends State<TabBarExample> {
|
||||
int? focusedIndex;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
initialIndex: 1,
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('TabBar Sample'),
|
||||
bottom: TabBar(
|
||||
onFocusChange: (bool value, int index) {
|
||||
setState(() {
|
||||
focusedIndex = switch (value) {
|
||||
true => index,
|
||||
false => null,
|
||||
};
|
||||
});
|
||||
},
|
||||
tabs: <Widget>[
|
||||
Tab(icon: Icon(Icons.cloud_outlined, size: focusedIndex == 0 ? 35 : 25)),
|
||||
Tab(icon: Icon(Icons.beach_access_sharp, size: focusedIndex == 1 ? 35 : 25)),
|
||||
Tab(icon: Icon(Icons.brightness_5_sharp, size: focusedIndex == 2 ? 35 : 25)),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: const TabBarView(
|
||||
children: <Widget>[
|
||||
Center(child: Text("It's cloudy here")),
|
||||
Center(child: Text("It's rainy here")),
|
||||
Center(child: Text("It's sunny here")),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
64
examples/api/lib/material/tabs/tab_bar.onHover.dart
Normal file
64
examples/api/lib/material/tabs/tab_bar.onHover.dart
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Flutter code sample for [TabBar.onFocusChange].
|
||||
|
||||
void main() => runApp(const TabBarApp());
|
||||
|
||||
class TabBarApp extends StatelessWidget {
|
||||
const TabBarApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(theme: ThemeData(useMaterial3: true), home: const TabBarExample());
|
||||
}
|
||||
}
|
||||
|
||||
class TabBarExample extends StatefulWidget {
|
||||
const TabBarExample({super.key});
|
||||
|
||||
@override
|
||||
State<TabBarExample> createState() => _TabBarExampleState();
|
||||
}
|
||||
|
||||
class _TabBarExampleState extends State<TabBarExample> {
|
||||
final List<Color> tabColors = <Color>[Colors.purple, Colors.purple, Colors.purple];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
initialIndex: 1,
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('TabBar Sample'),
|
||||
bottom: TabBar(
|
||||
onHover: (bool value, int index) {
|
||||
setState(() {
|
||||
tabColors[index] = switch (value) {
|
||||
true => Colors.pink,
|
||||
false => Colors.purple,
|
||||
};
|
||||
});
|
||||
},
|
||||
tabs: <Widget>[
|
||||
Tab(icon: Icon(Icons.cloud_outlined, color: tabColors[0])),
|
||||
Tab(icon: Icon(Icons.beach_access_sharp, color: tabColors[1])),
|
||||
Tab(icon: Icon(Icons.brightness_5_sharp, color: tabColors[2])),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: const TabBarView(
|
||||
children: <Widget>[
|
||||
Center(child: Text("It's cloudy here")),
|
||||
Center(child: Text("It's rainy here")),
|
||||
Center(child: Text("It's sunny here")),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_api_samples/material/tabs/tab_bar.onFocusChange.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Tabs change in response to focus', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const example.TabBarApp());
|
||||
|
||||
final TabBar tabBar = tester.widget<TabBar>(find.byType(TabBar));
|
||||
expect(tabBar.tabs.length, 3);
|
||||
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.cloud_outlined)).size, 25);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.beach_access_sharp)).size, 25);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.brightness_5_sharp)).size, 25);
|
||||
|
||||
// Focus on the first tab.
|
||||
Element tabElement = tester.element(find.byIcon(Icons.cloud_outlined));
|
||||
FocusNode node = Focus.of(tabElement);
|
||||
node.requestFocus();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.cloud_outlined)).size, 35);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.beach_access_sharp)).size, 25);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.brightness_5_sharp)).size, 25);
|
||||
|
||||
// Move focus to the second tab
|
||||
tabElement = tester.element(find.byIcon(Icons.beach_access_sharp));
|
||||
node = Focus.of(tabElement);
|
||||
node.requestFocus();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.cloud_outlined)).size, 25);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.beach_access_sharp)).size, 35);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.brightness_5_sharp)).size, 25);
|
||||
|
||||
// And the third
|
||||
tabElement = tester.element(find.byIcon(Icons.brightness_5_sharp));
|
||||
node = Focus.of(tabElement);
|
||||
node.requestFocus();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.cloud_outlined)).size, 25);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.beach_access_sharp)).size, 25);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.brightness_5_sharp)).size, 35);
|
||||
|
||||
// Unfocus
|
||||
node.unfocus();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.cloud_outlined)).size, 25);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.beach_access_sharp)).size, 25);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.brightness_5_sharp)).size, 25);
|
||||
});
|
||||
}
|
58
examples/api/test/material/tabs/tab_bar.onHover_test.dart
Normal file
58
examples/api/test/material/tabs/tab_bar.onHover_test.dart
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_api_samples/material/tabs/tab_bar.onHover.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Tabs change in response to hover', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const example.TabBarApp());
|
||||
|
||||
final TabBar tabBar = tester.widget<TabBar>(find.byType(TabBar));
|
||||
expect(tabBar.tabs.length, 3);
|
||||
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.cloud_outlined)).color, Colors.purple);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.beach_access_sharp)).color, Colors.purple);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.brightness_5_sharp)).color, Colors.purple);
|
||||
|
||||
// Hover over the first tab.
|
||||
final TestGesture gesture = await tester.createGesture(
|
||||
kind: PointerDeviceKind.mouse,
|
||||
pointer: 1,
|
||||
);
|
||||
await gesture.addPointer();
|
||||
await gesture.moveTo(tester.getCenter(find.byIcon(Icons.cloud_outlined)));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.cloud_outlined)).color, Colors.pink);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.beach_access_sharp)).color, Colors.purple);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.brightness_5_sharp)).color, Colors.purple);
|
||||
|
||||
// Hover over the second tab
|
||||
await gesture.moveTo(tester.getCenter(find.byIcon(Icons.beach_access_sharp)));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.cloud_outlined)).color, Colors.purple);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.beach_access_sharp)).color, Colors.pink);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.brightness_5_sharp)).color, Colors.purple);
|
||||
|
||||
// And the third
|
||||
await gesture.moveTo(tester.getCenter(find.byIcon(Icons.brightness_5_sharp)));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.cloud_outlined)).color, Colors.purple);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.beach_access_sharp)).color, Colors.purple);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.brightness_5_sharp)).color, Colors.pink);
|
||||
|
||||
// Remove hover
|
||||
await gesture.removePointer();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.cloud_outlined)).color, Colors.purple);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.beach_access_sharp)).color, Colors.purple);
|
||||
expect(tester.widget<Icon>(find.byIcon(Icons.brightness_5_sharp)).color, Colors.purple);
|
||||
});
|
||||
}
|
@ -844,6 +844,15 @@ class _TabBarScrollController extends ScrollController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Signature for [TabBar] callbacks that report that an underlying value has
|
||||
/// changed for a given [Tab] at `index`.
|
||||
///
|
||||
/// Used for [TabBar.onHover] and [TabBar.onFocusChange] callbacks The provided
|
||||
/// `value` being true indicates focus has been gained, or a pointer has hovered
|
||||
/// over the tab, with false indicated focus has been lost or the pointer has
|
||||
/// exited hovering.
|
||||
typedef TabValueChanged<T> = void Function(T value, int index);
|
||||
|
||||
/// A Material Design primary tab bar.
|
||||
///
|
||||
/// Primary tabs are placed at the top of the content pane under a top app bar.
|
||||
@ -932,6 +941,8 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
this.mouseCursor,
|
||||
this.enableFeedback,
|
||||
this.onTap,
|
||||
this.onHover,
|
||||
this.onFocusChange,
|
||||
this.physics,
|
||||
this.splashFactory,
|
||||
this.splashBorderRadius,
|
||||
@ -985,6 +996,8 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
this.mouseCursor,
|
||||
this.enableFeedback,
|
||||
this.onTap,
|
||||
this.onHover,
|
||||
this.onFocusChange,
|
||||
this.physics,
|
||||
this.splashFactory,
|
||||
this.splashBorderRadius,
|
||||
@ -1253,6 +1266,46 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
/// interfere with the default tap handler.
|
||||
final ValueChanged<int>? onTap;
|
||||
|
||||
/// An optional callback that's called when a [Tab]'s hover state in the
|
||||
/// [TabBar] changes.
|
||||
///
|
||||
/// Called when a pointer enters or exits the ink response area of the [Tab].
|
||||
///
|
||||
/// The value passed to the callback is true if a pointer has entered the
|
||||
/// [Tab] at `index` and false if a pointer has exited.
|
||||
///
|
||||
/// When hover is moved from one tab directly to another, this will be called
|
||||
/// twice. First to represent hover exiting the initial tab, and then second
|
||||
/// for the pointer entering hover over the next tab.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample shows how to customize a [Tab] in response to hovering over a
|
||||
/// [TabBar].
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/tabs/tab_bar.onHover.dart **
|
||||
/// {@end-tool}
|
||||
final TabValueChanged<bool>? onHover;
|
||||
|
||||
/// An optional callback that's called when a [Tab]'s focus state in the
|
||||
/// [TabBar] changes.
|
||||
///
|
||||
/// Called when the node fo the [Tab] at `index` gains or loses focus.
|
||||
///
|
||||
/// The value passed to the callback is true if the node has gained focus for
|
||||
/// the [Tab] at `index` and false if focus has been lost.
|
||||
///
|
||||
/// When focus is moved from one tab directly to another, this will be called
|
||||
/// twice. First to represent focus being lost by the initially focused tab,
|
||||
/// and then second for the next tab gaining focus.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample shows how to customize a [Tab] based on focus traversal in
|
||||
/// enclosing [TabBar].
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/tabs/tab_bar.onFocusChange.dart **
|
||||
/// {@end-tool}
|
||||
final TabValueChanged<bool>? onFocusChange;
|
||||
|
||||
/// How the [TabBar]'s scroll view should respond to user input.
|
||||
///
|
||||
/// For example, determines how the scroll view continues to animate after the
|
||||
@ -1895,6 +1948,12 @@ class _TabBarState extends State<TabBar> {
|
||||
onTap: () {
|
||||
_handleTap(index);
|
||||
},
|
||||
onHover: (bool value) {
|
||||
widget.onHover?.call(value, index);
|
||||
},
|
||||
onFocusChange: (bool value) {
|
||||
widget.onFocusChange?.call(value, index);
|
||||
},
|
||||
enableFeedback: widget.enableFeedback ?? true,
|
||||
overlayColor: widget.overlayColor ?? tabBarTheme.overlayColor ?? defaultOverlay,
|
||||
splashFactory: widget.splashFactory ?? tabBarTheme.splashFactory ?? _defaults.splashFactory,
|
||||
|
@ -8861,4 +8861,283 @@ void main() {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('onHover is triggered when mouse pointer is over a tab', (WidgetTester tester) async {
|
||||
final List<({bool hover, int index})> hoverEvents = <({bool hover, int index})>[];
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
bottom: TabBar(
|
||||
onHover: (bool value, int index) {
|
||||
hoverEvents.add((hover: value, index: index));
|
||||
},
|
||||
tabs: const <Widget>[Tab(text: 'Tab 1'), Tab(text: 'Tab 2'), Tab(text: 'Tab 3')],
|
||||
),
|
||||
),
|
||||
body: const TabBarView(
|
||||
children: <Widget>[Text('Tab 1 View'), Text('Tab 2 View'), Text('Tab 3 View')],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(hoverEvents.isEmpty, isTrue);
|
||||
|
||||
// Hover over the first tab.
|
||||
final TestGesture gesture = await tester.createGesture(
|
||||
kind: PointerDeviceKind.mouse,
|
||||
pointer: 1,
|
||||
);
|
||||
await gesture.addPointer();
|
||||
await gesture.moveTo(tester.getCenter(find.text('Tab 1')));
|
||||
await tester.pump();
|
||||
|
||||
// Hover entered first tab.
|
||||
expect(hoverEvents, <({bool hover, int index})>[(hover: true, index: 0)]);
|
||||
|
||||
await gesture.moveTo(tester.getCenter(find.text('Tab 2')));
|
||||
await tester.pump();
|
||||
|
||||
expect(hoverEvents, <({bool hover, int index})>[
|
||||
(hover: true, index: 0), // First tab hover enter
|
||||
(hover: false, index: 0), // First tab hover exit
|
||||
(hover: true, index: 1), // Second tab hover enter
|
||||
]);
|
||||
|
||||
await gesture.moveTo(tester.getCenter(find.text('Tab 3')));
|
||||
await tester.pump();
|
||||
|
||||
expect(hoverEvents, <({bool hover, int index})>[
|
||||
(hover: true, index: 0), // First tab hover enter
|
||||
(hover: false, index: 0), // First tab hover exit
|
||||
(hover: true, index: 1), // Second tab hover enter
|
||||
(hover: false, index: 1), // Second tab hover exit
|
||||
(hover: true, index: 2), // Third tab hover enter
|
||||
]);
|
||||
|
||||
await gesture.moveTo(tester.getCenter(find.byType(TabBarView)));
|
||||
await tester.pump();
|
||||
|
||||
expect(hoverEvents, <({bool hover, int index})>[
|
||||
(hover: true, index: 0), // First tab hover enter
|
||||
(hover: false, index: 0), // First tab hover exit
|
||||
(hover: true, index: 1), // Second tab hover enter
|
||||
(hover: false, index: 1), // Second tab hover exit
|
||||
(hover: true, index: 2), // Third tab hover enter
|
||||
(hover: false, index: 2), // Third tab hover exit
|
||||
]);
|
||||
|
||||
hoverEvents.clear();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
bottom: TabBar.secondary(
|
||||
onHover: (bool value, int index) {
|
||||
hoverEvents.add((hover: value, index: index));
|
||||
},
|
||||
tabs: const <Widget>[Tab(text: 'Tab 1'), Tab(text: 'Tab 2'), Tab(text: 'Tab 3')],
|
||||
),
|
||||
),
|
||||
body: const TabBarView(
|
||||
children: <Widget>[Text('Tab 1 View'), Text('Tab 2 View'), Text('Tab 3 View')],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(hoverEvents.isEmpty, isTrue);
|
||||
|
||||
// Hover over the first tab.
|
||||
await gesture.moveTo(tester.getCenter(find.text('Tab 1')));
|
||||
await tester.pump();
|
||||
|
||||
// Hover enters first tab.
|
||||
expect(hoverEvents, <({bool hover, int index})>[(hover: true, index: 0)]);
|
||||
|
||||
await gesture.moveTo(tester.getCenter(find.text('Tab 2')));
|
||||
await tester.pump();
|
||||
|
||||
expect(hoverEvents, <({bool hover, int index})>[
|
||||
(hover: true, index: 0), // First tab hover enter
|
||||
(hover: false, index: 0), // First tab hover exit
|
||||
(hover: true, index: 1), // Second tab hover enter
|
||||
]);
|
||||
|
||||
await gesture.moveTo(tester.getCenter(find.text('Tab 3')));
|
||||
await tester.pump();
|
||||
|
||||
expect(hoverEvents, <({bool hover, int index})>[
|
||||
(hover: true, index: 0), // First tab hover enter
|
||||
(hover: false, index: 0), // First tab hover exit
|
||||
(hover: true, index: 1), // Second tab hover enter
|
||||
(hover: false, index: 1), // Second tab hover exit
|
||||
(hover: true, index: 2), // Third tab hover enter
|
||||
]);
|
||||
|
||||
await gesture.moveTo(tester.getCenter(find.byType(TabBarView)));
|
||||
await tester.pump();
|
||||
|
||||
expect(hoverEvents, <({bool hover, int index})>[
|
||||
(hover: true, index: 0), // First tab hover enter
|
||||
(hover: false, index: 0), // First tab hover exit
|
||||
(hover: true, index: 1), // Second tab hover enter
|
||||
(hover: false, index: 1), // Second tab hover exit
|
||||
(hover: true, index: 2), // Third tab hover enter
|
||||
(hover: false, index: 2), // Third tab hover exit
|
||||
]);
|
||||
});
|
||||
|
||||
testWidgets('onFocusChange is triggered when tabs gain and lose focus', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final List<({bool focus, int index})> focusEvents = <({bool focus, int index})>[];
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
bottom: TabBar(
|
||||
onFocusChange: (bool value, int index) {
|
||||
focusEvents.add((focus: value, index: index));
|
||||
},
|
||||
tabs: const <Widget>[Tab(text: 'Tab 1'), Tab(text: 'Tab 2'), Tab(text: 'Tab 3')],
|
||||
),
|
||||
),
|
||||
body: const TabBarView(
|
||||
children: <Widget>[Text('Tab 1 View'), Text('Tab 2 View'), Text('Tab 3 View')],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(focusEvents.isEmpty, isTrue);
|
||||
|
||||
// Focus on the first tab.
|
||||
Element tabElement = tester.element(find.text('Tab 1'));
|
||||
FocusNode node = Focus.of(tabElement);
|
||||
node.requestFocus();
|
||||
await tester.pump();
|
||||
|
||||
// Focus gained at first tab.
|
||||
expect(focusEvents, <({bool focus, int index})>[(focus: true, index: 0)]);
|
||||
|
||||
tabElement = tester.element(find.text('Tab 2'));
|
||||
node = Focus.of(tabElement);
|
||||
node.requestFocus();
|
||||
await tester.pump();
|
||||
|
||||
expect(focusEvents, <({bool focus, int index})>[
|
||||
(focus: true, index: 0), // First tab gains focus
|
||||
(focus: false, index: 0), // First tab loses focus
|
||||
(focus: true, index: 1), // Second tab gains focus
|
||||
]);
|
||||
|
||||
tabElement = tester.element(find.text('Tab 3'));
|
||||
node = Focus.of(tabElement);
|
||||
node.requestFocus();
|
||||
await tester.pump();
|
||||
expect(node.hasFocus, isTrue);
|
||||
expect(focusEvents, <({bool focus, int index})>[
|
||||
(focus: true, index: 0), // First tab gains focus
|
||||
(focus: false, index: 0), // First tab loses focus
|
||||
(focus: true, index: 1), // Second tab gains focus
|
||||
(focus: false, index: 1), // Second tab loses focus
|
||||
(focus: true, index: 2), // Third tab gains focus
|
||||
]);
|
||||
|
||||
node.unfocus();
|
||||
await tester.pump();
|
||||
|
||||
expect(node.hasFocus, isFalse);
|
||||
expect(focusEvents, <({bool focus, int index})>[
|
||||
(focus: true, index: 0), // First tab gains focus
|
||||
(focus: false, index: 0), // First tab loses focus
|
||||
(focus: true, index: 1), // Second tab gains focus
|
||||
(focus: false, index: 1), // Second tab loses focus
|
||||
(focus: true, index: 2), // Third tab gains focus
|
||||
(focus: false, index: 2), // Third tab loses focus
|
||||
]);
|
||||
|
||||
focusEvents.clear();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
bottom: TabBar.secondary(
|
||||
onFocusChange: (bool value, int index) {
|
||||
focusEvents.add((focus: value, index: index));
|
||||
},
|
||||
tabs: const <Widget>[Tab(text: 'Tab 1'), Tab(text: 'Tab 2'), Tab(text: 'Tab 3')],
|
||||
),
|
||||
),
|
||||
body: const TabBarView(
|
||||
children: <Widget>[Text('Tab 1 View'), Text('Tab 2 View'), Text('Tab 3 View')],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(focusEvents.isEmpty, isTrue);
|
||||
|
||||
// Focus on the first tab.
|
||||
tabElement = tester.element(find.text('Tab 1'));
|
||||
node = Focus.of(tabElement);
|
||||
node.requestFocus();
|
||||
await tester.pump();
|
||||
|
||||
// Focus gained at first tab.
|
||||
expect(focusEvents, <({bool focus, int index})>[(focus: true, index: 0)]);
|
||||
|
||||
tabElement = tester.element(find.text('Tab 2'));
|
||||
node = Focus.of(tabElement);
|
||||
node.requestFocus();
|
||||
await tester.pump();
|
||||
|
||||
expect(focusEvents, <({bool focus, int index})>[
|
||||
(focus: true, index: 0), // First tab gains focus
|
||||
(focus: false, index: 0), // First tab loses focus
|
||||
(focus: true, index: 1), // Second tab gains focus
|
||||
]);
|
||||
|
||||
tabElement = tester.element(find.text('Tab 3'));
|
||||
node = Focus.of(tabElement);
|
||||
node.requestFocus();
|
||||
await tester.pump();
|
||||
expect(node.hasFocus, isTrue);
|
||||
expect(focusEvents, <({bool focus, int index})>[
|
||||
(focus: true, index: 0), // First tab gains focus
|
||||
(focus: false, index: 0), // First tab loses focus
|
||||
(focus: true, index: 1), // Second tab gains focus
|
||||
(focus: false, index: 1), // Second tab loses focus
|
||||
(focus: true, index: 2), // Third tab gains focus
|
||||
]);
|
||||
|
||||
node.unfocus();
|
||||
await tester.pump();
|
||||
|
||||
expect(node.hasFocus, isFalse);
|
||||
expect(focusEvents, <({bool focus, int index})>[
|
||||
(focus: true, index: 0), // First tab gains focus
|
||||
(focus: false, index: 0), // First tab loses focus
|
||||
(focus: true, index: 1), // Second tab gains focus
|
||||
(focus: false, index: 1), // Second tab loses focus
|
||||
(focus: true, index: 2), // Third tab gains focus
|
||||
(focus: false, index: 2), // Third tab loses focus
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user