Unverified Commit 02d5c759 authored by Pierre-Louis's avatar Pierre-Louis Committed by GitHub

Add support for secondary tab bar (#122756)

Add support for secondary tab bar
parent ce68d979
......@@ -12,8 +12,8 @@ class TabsTemplate extends TokenTemplate {
@override
String generate() => '''
class _${blockName}DefaultsM3 extends TabBarTheme {
_${blockName}DefaultsM3(this.context)
class _${blockName}PrimaryDefaultsM3 extends TabBarTheme {
_${blockName}PrimaryDefaultsM3(this.context)
: super(indicatorSize: TabBarIndicatorSize.label);
final BuildContext context;
......@@ -69,5 +69,64 @@ class _${blockName}DefaultsM3 extends TabBarTheme {
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
}
class _${blockName}SecondaryDefaultsM3 extends TabBarTheme {
_${blockName}SecondaryDefaultsM3(this.context)
: super(indicatorSize: TabBarIndicatorSize.tab);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
late final TextTheme _textTheme = Theme.of(context).textTheme;
@override
Color? get dividerColor => ${componentColor("md.comp.secondary-navigation-tab.divider")};
@override
Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")};
@override
Color? get labelColor => ${componentColor("md.comp.secondary-navigation-tab.active.label-text")};
@override
TextStyle? get labelStyle => ${textStyle("md.comp.secondary-navigation-tab.label-text")};
@override
Color? get unselectedLabelColor => ${componentColor("md.comp.secondary-navigation-tab.inactive.label-text")};
@override
TextStyle? get unselectedLabelStyle => ${textStyle("md.comp.secondary-navigation-tab.label-text")};
@override
MaterialStateProperty<Color?> get overlayColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.secondary-navigation-tab.hover.state-layer')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.secondary-navigation-tab.focus.state-layer')};
}
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.secondary-navigation-tab.pressed.state-layer')};
}
return null;
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.secondary-navigation-tab.hover.state-layer')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.secondary-navigation-tab.focus.state-layer')};
}
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.secondary-navigation-tab.pressed.state-layer')};
}
return null;
});
}
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
}
''';
}
......@@ -6,24 +6,22 @@
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
void main() => runApp(const TabBarApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String _title = 'Flutter Code Sample';
class TabBarApp extends StatelessWidget {
const TabBarApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatelessWidget(),
return MaterialApp(
theme: ThemeData(useMaterial3: true),
home: const TabBarExample(),
);
}
}
class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({super.key});
class TabBarExample extends StatelessWidget {
const TabBarExample({super.key});
@override
Widget build(BuildContext context) {
......@@ -32,7 +30,7 @@ class MyStatelessWidget extends StatelessWidget {
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('TabBar Widget'),
title: const Text('TabBar Sample'),
bottom: const TabBar(
tabs: <Widget>[
Tab(
......
......@@ -6,34 +6,31 @@
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
void main() => runApp(const TabBarApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String _title = 'Flutter Code Sample';
class TabBarApp extends StatelessWidget {
const TabBarApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatefulWidget(),
return MaterialApp(
theme: ThemeData(useMaterial3: true),
home: const TabBarExample(),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({super.key});
class TabBarExample extends StatefulWidget {
const TabBarExample({super.key});
@override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
State<TabBarExample> createState() => _TabBarExampleState();
}
/// [AnimationController]s can be created with `vsync: this` because of
/// [TickerProviderStateMixin].
class _MyStatefulWidgetState extends State<MyStatefulWidget>
with TickerProviderStateMixin {
late TabController _tabController;
class _TabBarExampleState extends State<TabBarExample> with TickerProviderStateMixin {
late final TabController _tabController;
@override
void initState() {
......@@ -41,11 +38,17 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget>
_tabController = TabController(length: 3, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('TabBar Widget'),
title: const Text('TabBar Sample'),
bottom: TabBar(
controller: _tabController,
tabs: const <Widget>[
......
// 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.
// Flutter code sample for [TabBar].
import 'package:flutter/material.dart';
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 StatelessWidget {
const TabBarExample({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
initialIndex: 1,
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('Primary and secondary TabBar'),
bottom: const TabBar(
dividerColor: Colors.transparent,
tabs: <Widget>[
Tab(
text: 'Flights',
icon: Icon(Icons.flight),
),
Tab(
text: 'Trips',
icon: Icon(Icons.luggage),
),
Tab(
text: 'Explore',
icon: Icon(Icons.explore),
),
],
),
),
body: const TabBarView(
children: <Widget>[
NestedTabBar('Flights'),
NestedTabBar('Trips'),
NestedTabBar('Explore'),
],
),
),
);
}
}
class NestedTabBar extends StatefulWidget {
const NestedTabBar(this.outerTab, {super.key});
final String outerTab;
@override
State<NestedTabBar> createState() => _NestedTabBarState();
}
class _NestedTabBarState extends State<NestedTabBar> with TickerProviderStateMixin {
late final TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
TabBar.secondary(
controller: _tabController,
tabs: const <Widget>[
Tab(text: 'Overview'),
Tab(text: 'Specifications'),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: <Widget>[
Card(
margin: const EdgeInsets.all(16.0),
child: Center(child: Text('${widget.outerTab}: Overview tab')),
),
Card(
margin: const EdgeInsets.all(16.0),
child: Center(child: Text('${widget.outerTab}: Specifications tab')),
),
],
),
),
],
);
}
}
// 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.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Switch tabs in the TabBar', (WidgetTester tester) async {
await tester.pumpWidget(
const example.TabBarApp(),
);
final TabBar tabBar = tester.widget<TabBar>(find.byType(TabBar));
expect(tabBar.tabs.length, 3);
final Finder tab1 = find.widgetWithIcon(Tab, Icons.cloud_outlined);
final Finder tab2 = find.widgetWithIcon(Tab, Icons.beach_access_sharp);
final Finder tab3 = find.widgetWithIcon(Tab, Icons.brightness_5_sharp);
const String tabBarViewText1 = "It's cloudy here";
const String tabBarViewText2 = "It's rainy here";
const String tabBarViewText3 = "It's sunny here";
expect(find.text(tabBarViewText1), findsNothing);
expect(find.text(tabBarViewText2), findsOneWidget);
expect(find.text(tabBarViewText3), findsNothing);
await tester.tap(tab1);
await tester.pumpAndSettle();
expect(find.text(tabBarViewText1), findsOneWidget);
expect(find.text(tabBarViewText2), findsNothing);
expect(find.text(tabBarViewText3), findsNothing);
await tester.tap(tab2);
await tester.pumpAndSettle();
expect(find.text(tabBarViewText1), findsNothing);
expect(find.text(tabBarViewText2), findsOneWidget);
expect(find.text(tabBarViewText3), findsNothing);
await tester.tap(tab3);
await tester.pumpAndSettle();
expect(find.text(tabBarViewText1), findsNothing);
expect(find.text(tabBarViewText2), findsNothing);
expect(find.text(tabBarViewText3), findsOneWidget);
});
}
// 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.1.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Switch tabs in the TabBar', (WidgetTester tester) async {
await tester.pumpWidget(
const example.TabBarApp(),
);
final TabBar tabBar = tester.widget<TabBar>(find.byType(TabBar));
expect(tabBar.tabs.length, 3);
final Finder tab1 = find.widgetWithIcon(Tab, Icons.cloud_outlined);
final Finder tab2 = find.widgetWithIcon(Tab, Icons.beach_access_sharp);
final Finder tab3 = find.widgetWithIcon(Tab, Icons.brightness_5_sharp);
const String tabBarViewText1 = "It's cloudy here";
const String tabBarViewText2 = "It's rainy here";
const String tabBarViewText3 = "It's sunny here";
expect(find.text(tabBarViewText1), findsOneWidget);
expect(find.text(tabBarViewText2), findsNothing);
expect(find.text(tabBarViewText3), findsNothing);
await tester.tap(tab1);
await tester.pumpAndSettle();
expect(find.text(tabBarViewText1), findsOneWidget);
expect(find.text(tabBarViewText2), findsNothing);
expect(find.text(tabBarViewText3), findsNothing);
await tester.tap(tab2);
await tester.pumpAndSettle();
expect(find.text(tabBarViewText1), findsNothing);
expect(find.text(tabBarViewText2), findsOneWidget);
expect(find.text(tabBarViewText3), findsNothing);
await tester.tap(tab3);
await tester.pumpAndSettle();
expect(find.text(tabBarViewText1), findsNothing);
expect(find.text(tabBarViewText2), findsNothing);
expect(find.text(tabBarViewText3), findsOneWidget);
});
}
// 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.2.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Switch tabs in the TabBar', (WidgetTester tester) async {
const String primaryTabLabel1 = 'Flights';
const String primaryTabLabel2 = 'Trips';
const String primaryTabLabel3 = 'Explore';
const String secondaryTabLabel1 = 'Overview';
const String secondaryTabLabel2 = 'Specifications';
await tester.pumpWidget(
const example.TabBarApp(),
);
final TabBar primaryTabBar = tester.widget<TabBar>(find.byType(TabBar).last);
expect(primaryTabBar.tabs.length, 3);
final TabBar secondaryTabBar = tester.widget<TabBar>(find.byType(TabBar).first);
expect(secondaryTabBar.tabs.length, 2);
final Finder primaryTab1 = find.widgetWithText(Tab, primaryTabLabel1);
final Finder primaryTab2 = find.widgetWithText(Tab, primaryTabLabel2);
final Finder primaryTab3 = find.widgetWithText(Tab, primaryTabLabel3);
final Finder secondaryTab2 = find.widgetWithText(Tab, secondaryTabLabel2);
String tabBarViewText = '$primaryTabLabel2: $secondaryTabLabel1 tab';
expect(find.text(tabBarViewText), findsOneWidget);
await tester.tap(primaryTab1);
await tester.pumpAndSettle();
tabBarViewText = '$primaryTabLabel1: $secondaryTabLabel1 tab';
expect(find.text(tabBarViewText), findsOneWidget);
await tester.tap(secondaryTab2);
await tester.pumpAndSettle();
tabBarViewText = '$primaryTabLabel1: $secondaryTabLabel2 tab';
expect(find.text(tabBarViewText), findsOneWidget);
await tester.tap(primaryTab2);
await tester.pumpAndSettle();
tabBarViewText = '$primaryTabLabel2: $secondaryTabLabel1 tab';
expect(find.text(tabBarViewText), findsOneWidget);
await tester.tap(secondaryTab2);
await tester.pumpAndSettle();
tabBarViewText = '$primaryTabLabel2: $secondaryTabLabel2 tab';
expect(find.text(tabBarViewText), findsOneWidget);
await tester.tap(primaryTab3);
await tester.pumpAndSettle();
tabBarViewText = '$primaryTabLabel3: $secondaryTabLabel1 tab';
expect(find.text(tabBarViewText), findsOneWidget);
await tester.tap(secondaryTab2);
await tester.pumpAndSettle();
tabBarViewText = '$primaryTabLabel3: $secondaryTabLabel2 tab';
expect(find.text(tabBarViewText), findsOneWidget);
});
}
......@@ -167,24 +167,27 @@ class _TabStyle extends AnimatedWidget {
const _TabStyle({
required Animation<double> animation,
required this.isSelected,
required this.isPrimary,
required this.labelColor,
required this.unselectedLabelColor,
required this.labelStyle,
required this.unselectedLabelStyle,
required this.defaults,
required this.child,
}) : super(listenable: animation);
final TextStyle? labelStyle;
final TextStyle? unselectedLabelStyle;
final bool isSelected;
final bool isPrimary;
final Color? labelColor;
final Color? unselectedLabelColor;
final TabBarTheme defaults;
final Widget child;
MaterialStateColor _resolveWithLabelColor(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final TabBarTheme defaults = themeData.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
final Animation<double> animation = listenable as Animation<double>;
// labelStyle.color (and tabBarTheme.labelStyle.color) is not considered
......@@ -219,9 +222,7 @@ class _TabStyle extends AnimatedWidget {
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final TabBarTheme defaults = themeData.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
final Animation<double> animation = listenable as Animation<double>;
final Set<MaterialState> states = isSelected
......@@ -604,7 +605,10 @@ class _TabBarScrollController extends ScrollController {
}
}
/// A Material Design widget that displays a horizontal row of tabs.
/// A Material Design primary tab bar.
///
/// Primary tabs are placed at the top of the content pane under a top app bar.
/// They display the main content destinations.
///
/// Typically created as the [AppBar.bottom] part of an [AppBar] and in
/// conjunction with a [TabBarView].
......@@ -635,12 +639,23 @@ class _TabBarScrollController extends ScrollController {
/// ** See code in examples/api/lib/material/tabs/tab_bar.1.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This sample showcases nested Material 3 [TabBar]s. It consists of a primary
/// [TabBar] with nested a secondary [TabBar]. The primary [TabBar] uses a
/// [DefaultTabController] while the secondary [TabBar] uses a [TabController].
///
/// ** See code in examples/api/lib/material/tabs/tab_bar.2.dart **
/// {@end-tool}
///
/// See also:
///
/// * [TabBar.secondary], for a secondary tab bar.
/// * [TabBarView], which displays page views that correspond to each tab.
/// * [TabController], which coordinates tab selection between a [TabBar] and a [TabBarView].
/// * https://m3.material.io/components/tab-bar/overview, the Material 3
/// tab bar specification.
class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// Creates a Material Design tab bar.
/// Creates a Material Design primary tab bar.
///
/// The [tabs] argument must not be null and its length must match the [controller]'s
/// [TabController.length].
......@@ -680,7 +695,57 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.physics,
this.splashFactory,
this.splashBorderRadius,
}) : assert(indicator != null || (indicatorWeight > 0.0));
}) : _isPrimary = true,
assert(indicator != null || (indicatorWeight > 0.0));
/// Creates a Material Design secondary tab bar.
///
/// Secondary tabs are used within a content area to further separate related
/// content and establish hierarchy.
///
/// {@tool dartpad}
/// This sample showcases nested Material 3 [TabBar]s. It consists of a primary
/// [TabBar] with nested a secondary [TabBar]. The primary [TabBar] uses a
/// [DefaultTabController] while the secondary [TabBar] uses a [TabController].
///
/// ** See code in examples/api/lib/material/tabs/tab_bar.2.dart **
/// {@end-tool}
///
/// See also:
///
/// * [TabBar], for a primary tab bar.
/// * [TabBarView], which displays page views that correspond to each tab.
/// * [TabController], which coordinates tab selection between a [TabBar] and a [TabBarView].
/// * https://m3.material.io/components/tab-bar/overview, the Material 3
/// tab bar specification.
const TabBar.secondary({
super.key,
required this.tabs,
this.controller,
this.isScrollable = false,
this.padding,
this.indicatorColor,
this.automaticIndicatorColorAdjustment = true,
this.indicatorWeight = 2.0,
this.indicatorPadding = EdgeInsets.zero,
this.indicator,
this.indicatorSize,
this.dividerColor,
this.labelColor,
this.labelStyle,
this.labelPadding,
this.unselectedLabelColor,
this.unselectedLabelStyle,
this.dragStartBehavior = DragStartBehavior.start,
this.overlayColor,
this.mouseCursor,
this.enableFeedback,
this.onTap,
this.physics,
this.splashFactory,
this.splashBorderRadius,
}) : _isPrimary = false,
assert(indicator != null || (indicatorWeight > 0.0));
/// Typically a list of two or more [Tab] widgets.
///
......@@ -993,6 +1058,11 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
return false;
}
/// Whether this tab bar is a primary tab bar.
///
/// Otherwise, it is a secondary tab bar.
final bool _isPrimary;
@override
State<TabBar> createState() => _TabBarState();
}
......@@ -1016,10 +1086,19 @@ class _TabBarState extends State<TabBar> {
_labelPaddings = List<EdgeInsetsGeometry>.filled(widget.tabs.length, EdgeInsets.zero, growable: true);
}
TabBarTheme get _defaults {
if (Theme.of(context).useMaterial3) {
return widget._isPrimary
? _TabsPrimaryDefaultsM3(context)
: _TabsSecondaryDefaultsM3(context);
} else {
return _TabsDefaultsM2(context);
}
}
Decoration _getIndicator() {
final ThemeData theme = Theme.of(context);
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final TabBarTheme defaults = theme.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
if (widget.indicator != null) {
return widget.indicator!;
......@@ -1030,7 +1109,7 @@ class _TabBarState extends State<TabBar> {
Color color = widget.indicatorColor
?? (theme.useMaterial3
? tabBarTheme.indicatorColor ?? defaults.indicatorColor!
? tabBarTheme.indicatorColor ?? _defaults.indicatorColor!
: Theme.of(context).indicatorColor);
// ThemeData tries to avoid this by having indicatorColor avoid being the
// primaryColor. However, it's possible that the tab bar is on a
......@@ -1046,12 +1125,13 @@ class _TabBarState extends State<TabBar> {
// TODO(xu-baolin): Remove automatic adjustment to white color indicator
// with a better long-term solution.
// https://github.com/flutter/flutter/pull/68171#pullrequestreview-517753917
if (widget.automaticIndicatorColorAdjustment && color.value == Material.maybeOf(context)?.color?.value) {
if (widget.automaticIndicatorColorAdjustment &&
color.value == Material.maybeOf(context)?.color?.value) {
color = Colors.white;
}
return UnderlineTabIndicator(
borderRadius: theme.useMaterial3
borderRadius: theme.useMaterial3 && widget._isPrimary
// TODO(tahatesser): Make sure this value matches Material 3 Tabs spec
// when `preferredSize`and `indicatorWeight` are updated to support Material 3
// https://m3.material.io/components/tabs/specs#149a189f-9039-4195-99da-15c205d20e30,
......@@ -1107,16 +1187,15 @@ class _TabBarState extends State<TabBar> {
void _initIndicatorPainter() {
final ThemeData theme = Theme.of(context);
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final TabBarTheme defaults = theme.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
_indicatorPainter = !_controllerIsValid ? null : _IndicatorPainter(
controller: _controller!,
indicator: _getIndicator(),
indicatorSize: widget.indicatorSize ?? tabBarTheme.indicatorSize ?? defaults.indicatorSize!,
indicatorSize: widget.indicatorSize ?? tabBarTheme.indicatorSize ?? _defaults.indicatorSize!,
indicatorPadding: widget.indicatorPadding,
tabKeys: _tabKeys,
old: _indicatorPainter,
dividerColor: theme.useMaterial3 ? widget.dividerColor ?? tabBarTheme.dividerColor ?? defaults.dividerColor : null,
dividerColor: theme.useMaterial3 ? widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor : null,
labelPaddings: _labelPaddings,
);
}
......@@ -1262,14 +1341,16 @@ class _TabBarState extends State<TabBar> {
widget.onTap?.call(index);
}
Widget _buildStyledTab(Widget child, bool isSelected, Animation<double> animation) {
Widget _buildStyledTab(Widget child, bool isSelected, Animation<double> animation, TabBarTheme defaults) {
return _TabStyle(
animation: animation,
isSelected: isSelected,
isPrimary: widget._isPrimary,
labelColor: widget.labelColor,
unselectedLabelColor: widget.unselectedLabelColor,
labelStyle: widget.labelStyle,
unselectedLabelStyle: widget.unselectedLabelStyle,
defaults: defaults,
child: child,
);
}
......@@ -1309,9 +1390,7 @@ class _TabBarState extends State<TabBar> {
);
}
final ThemeData theme = Theme.of(context);
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final TabBarTheme defaults = theme.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
final List<Widget> wrappedTabs = List<Widget>.generate(widget.tabs.length, (int index) {
const double verticalAdjustment = (_kTextAndIconTabHeight - _kTabHeight)/2.0;
......@@ -1353,22 +1432,22 @@ class _TabBarState extends State<TabBar> {
// The user tapped on a tab, the tab controller's animation is running.
assert(_currentIndex != previousIndex);
final Animation<double> animation = _ChangeAnimation(_controller!);
wrappedTabs[_currentIndex!] = _buildStyledTab(wrappedTabs[_currentIndex!], true, animation);
wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation);
wrappedTabs[_currentIndex!] = _buildStyledTab(wrappedTabs[_currentIndex!], true, animation, _defaults);
wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation, _defaults);
} else {
// The user is dragging the TabBarView's PageView left or right.
final int tabIndex = _currentIndex!;
final Animation<double> centerAnimation = _DragAnimation(_controller!, tabIndex);
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation);
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation, _defaults);
if (_currentIndex! > 0) {
final int tabIndex = _currentIndex! - 1;
final Animation<double> previousAnimation = ReverseAnimation(_DragAnimation(_controller!, tabIndex));
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, previousAnimation);
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, previousAnimation, _defaults);
}
if (_currentIndex! < widget.tabs.length - 1) {
final int tabIndex = _currentIndex! + 1;
final Animation<double> nextAnimation = ReverseAnimation(_DragAnimation(_controller!, tabIndex));
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, nextAnimation);
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, nextAnimation, _defaults);
}
}
}
......@@ -1389,7 +1468,7 @@ class _TabBarState extends State<TabBar> {
final MaterialStateProperty<Color?> defaultOverlay = MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
final Set<MaterialState> effectiveStates = selectedState..addAll(states);
return defaults.overlayColor?.resolve(effectiveStates);
return _defaults.overlayColor?.resolve(effectiveStates);
},
);
wrappedTabs[index] = InkWell(
......@@ -1397,7 +1476,7 @@ class _TabBarState extends State<TabBar> {
onTap: () { _handleTap(index); },
enableFeedback: widget.enableFeedback ?? true,
overlayColor: widget.overlayColor ?? tabBarTheme.overlayColor ?? defaultOverlay,
splashFactory: widget.splashFactory ?? tabBarTheme.splashFactory ?? defaults.splashFactory,
splashFactory: widget.splashFactory ?? tabBarTheme.splashFactory ?? _defaults.splashFactory,
borderRadius: widget.splashBorderRadius,
child: Padding(
padding: EdgeInsets.only(bottom: widget.indicatorWeight),
......@@ -1422,10 +1501,12 @@ class _TabBarState extends State<TabBar> {
child: _TabStyle(
animation: kAlwaysDismissedAnimation,
isSelected: false,
isPrimary: widget._isPrimary,
labelColor: widget.labelColor,
unselectedLabelColor: widget.unselectedLabelColor,
labelStyle: widget.labelStyle,
unselectedLabelStyle: widget.unselectedLabelStyle,
defaults: _defaults,
child: _TabLabelBar(
onPerformLayout: _saveTabOffsets,
children: wrappedTabs,
......@@ -1979,8 +2060,8 @@ class _TabsDefaultsM2 extends TabBarTheme {
// Token database version: v0_162
class _TabsDefaultsM3 extends TabBarTheme {
_TabsDefaultsM3(this.context)
class _TabsPrimaryDefaultsM3 extends TabBarTheme {
_TabsPrimaryDefaultsM3(this.context)
: super(indicatorSize: TabBarIndicatorSize.label);
final BuildContext context;
......@@ -2037,4 +2118,62 @@ class _TabsDefaultsM3 extends TabBarTheme {
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
}
class _TabsSecondaryDefaultsM3 extends TabBarTheme {
_TabsSecondaryDefaultsM3(this.context)
: super(indicatorSize: TabBarIndicatorSize.tab);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
late final TextTheme _textTheme = Theme.of(context).textTheme;
@override
Color? get dividerColor => _colors.surfaceVariant;
@override
Color? get indicatorColor => _colors.primary;
@override
Color? get labelColor => _colors.onSurface;
@override
TextStyle? get labelStyle => _textTheme.titleSmall;
@override
Color? get unselectedLabelColor => _colors.onSurfaceVariant;
@override
TextStyle? get unselectedLabelStyle => _textTheme.titleSmall;
@override
MaterialStateProperty<Color?> get overlayColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.hovered)) {
return _colors.onSurface.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return _colors.onSurface.withOpacity(0.12);
}
if (states.contains(MaterialState.pressed)) {
return _colors.onSurface.withOpacity(0.12);
}
return null;
}
if (states.contains(MaterialState.hovered)) {
return _colors.onSurface.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return _colors.onSurface.withOpacity(0.12);
}
if (states.contains(MaterialState.pressed)) {
return _colors.onSurface.withOpacity(0.12);
}
return null;
});
}
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
}
// END GENERATED TOKEN PROPERTIES - Tabs
......@@ -31,14 +31,30 @@ final List<SizedBox> _sizedTabs = <SizedBox>[
SizedBox(key: UniqueKey(), width: 100.0, height: 50.0),
];
Widget _withTheme(
TabBarTheme? theme, {
Widget buildTabBar({
TabBarTheme? tabBarTheme,
bool secondaryTabBar = false,
List<Widget> tabs = _tabs,
bool isScrollable = false,
bool useMaterial3 = false,
}) {
if (secondaryTabBar) {
return MaterialApp(
theme: ThemeData(tabBarTheme: theme, useMaterial3: useMaterial3),
theme: ThemeData(tabBarTheme: tabBarTheme, useMaterial3: useMaterial3),
home: Scaffold(
body: RepaintBoundary(
key: _painterKey,
child: TabBar.secondary(
tabs: tabs,
isScrollable: isScrollable,
controller: TabController(length: tabs.length, vsync: const TestVSync()),
),
),
),
);
}
return MaterialApp(
theme: ThemeData(tabBarTheme: tabBarTheme, useMaterial3: useMaterial3),
home: Scaffold(
body: RepaintBoundary(
key: _painterKey,
......@@ -52,12 +68,17 @@ Widget _withTheme(
);
}
RenderParagraph _iconRenderObject(WidgetTester tester, IconData icon) {
RenderParagraph _getIcon(WidgetTester tester, IconData icon) {
return tester.renderObject<RenderParagraph>(
find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)),
);
}
RenderParagraph _getText(WidgetTester tester, String text) {
return tester.renderObject<RenderParagraph>(find.text(text));
}
void main() {
test('TabBarTheme copyWith, ==, hashCode, defaults', () {
expect(const TabBarTheme(), const TabBarTheme().copyWith());
......@@ -82,60 +103,113 @@ void main() {
expect(identical(TabBarTheme.lerp(theme, theme, 0.5), theme), true);
});
testWidgets('Tab bar defaults', (WidgetTester tester) async {
// tests for the default label color and label styles when tabBarTheme and tabBar do not provide any
await tester.pumpWidget(_withTheme(null, useMaterial3: true));
testWidgets('Tab bar defaults (primary)', (WidgetTester tester) async {
// Test default label color and label styles.
await tester.pumpWidget(buildTabBar(useMaterial3: true));
final ThemeData theme = ThemeData(useMaterial3: true);
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
expect(selectedRenderObject.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
expect(selectedRenderObject.text.style!.fontSize, equals(14.0));
expect(selectedRenderObject.text.style!.color, equals(theme.colorScheme.primary));
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
expect(unselectedRenderObject.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
expect(unselectedRenderObject.text.style!.fontSize, equals(14.0));
expect(unselectedRenderObject.text.style!.color, equals(theme.colorScheme.onSurfaceVariant));
// tests for the default value of labelPadding when tabBarTheme and tabBar do not provide one
await tester.pumpWidget(_withTheme(null, tabs: _sizedTabs, isScrollable: true));
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
expect(selectedLabel.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
expect(selectedLabel.text.style!.fontSize, equals(14.0));
expect(selectedLabel.text.style!.color, equals(theme.colorScheme.primary));
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
expect(unselectedLabel.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
expect(unselectedLabel.text.style!.fontSize, equals(14.0));
expect(unselectedLabel.text.style!.color, equals(theme.colorScheme.onSurfaceVariant));
// Test default labelPadding.
await tester.pumpWidget(buildTabBar(tabs: _sizedTabs, isScrollable: true));
const double indicatorWeight = 2.0;
final Rect tabBar = tester.getRect(find.byType(TabBar));
final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!));
final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].key!));
// verify coordinates of tabOne
// Verify tabOne coordinates.
expect(tabOneRect.left, equals(kTabLabelPadding.left));
expect(tabOneRect.top, equals(kTabLabelPadding.top));
expect(tabOneRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight));
// verify coordinates of tabTwo
// Verify tabTwo coordinates.
expect(tabTwoRect.right, equals(tabBar.width - kTabLabelPadding.right));
expect(tabTwoRect.top, equals(kTabLabelPadding.top));
expect(tabTwoRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight));
// verify tabOne and tabTwo is separated by right padding of tabOne and left padding of tabTwo
// Verify tabOne and tabTwo is separated by right padding of tabOne and left padding of tabTwo.
expect(tabOneRect.right, equals(tabTwoRect.left - kTabLabelPadding.left - kTabLabelPadding.right));
// Verify divider color and indicator color.
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(
tabBarBox,
paints
..line(color: theme.colorScheme.surfaceVariant)
// Indicator is a rrect in the primary tab bar.
..rrect(color: theme.colorScheme.primary),
);
});
testWidgets('Tab bar defaults (secondary)', (WidgetTester tester) async {
// Test default label color and label styles.
await tester.pumpWidget(buildTabBar(secondaryTabBar: true, useMaterial3: true));
final ThemeData theme = ThemeData(useMaterial3: true);
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
expect(selectedLabel.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
expect(selectedLabel.text.style!.fontSize, equals(14.0));
expect(selectedLabel.text.style!.color, equals(theme.colorScheme.onSurface));
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
expect(unselectedLabel.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
expect(unselectedLabel.text.style!.fontSize, equals(14.0));
expect(unselectedLabel.text.style!.color, equals(theme.colorScheme.onSurfaceVariant));
// Test default labelPadding.
await tester.pumpWidget(buildTabBar(
secondaryTabBar: true,
tabs: _sizedTabs,
isScrollable: true,
useMaterial3: true,
));
const double indicatorWeight = 2.0;
final Rect tabBar = tester.getRect(find.byType(TabBar));
final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!));
final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].key!));
// Verify tabOne coordinates.
expect(tabOneRect.left, equals(kTabLabelPadding.left));
expect(tabOneRect.top, equals(kTabLabelPadding.top));
expect(tabOneRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight));
// Verify tabTwo coordinates.
expect(tabTwoRect.right, equals(tabBar.width - kTabLabelPadding.right));
expect(tabTwoRect.top, equals(kTabLabelPadding.top));
expect(tabTwoRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight));
// Verify tabOne and tabTwo is separated by right padding of tabOne and left padding of tabTwo.
expect(tabOneRect.right, equals(tabTwoRect.left - kTabLabelPadding.left - kTabLabelPadding.right));
// Verify divider color and indicator color.
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(
tabBarBox,
paints
..line(color: theme.colorScheme.surfaceVariant)
// Indicator is a line in the secondary tab bar.
..line(color: theme.colorScheme.primary),
);
});
testWidgets('Tab bar theme overrides label color (selected)', (WidgetTester tester) async {
const Color labelColor = Colors.black;
const TabBarTheme tabBarTheme = TabBarTheme(labelColor: labelColor);
await tester.pumpWidget(_withTheme(tabBarTheme));
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
final RenderParagraph textRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
expect(textRenderObject.text.style!.color, equals(labelColor));
final RenderParagraph iconRenderObject = _iconRenderObject(tester, Icons.looks_one);
expect(iconRenderObject.text.style!.color, equals(labelColor));
final RenderParagraph tabLabel = _getText(tester, _tab1Text);
expect(tabLabel.text.style!.color, equals(labelColor));
final RenderParagraph tabIcon = _getIcon(tester, Icons.looks_one);
expect(tabIcon.text.style!.color, equals(labelColor));
});
testWidgets('Tab bar theme overrides label padding', (WidgetTester tester) async {
......@@ -151,8 +225,8 @@ void main() {
const TabBarTheme tabBarTheme = TabBarTheme(labelPadding: labelPadding);
await tester.pumpWidget(_withTheme(
tabBarTheme,
await tester.pumpWidget(buildTabBar(
tabBarTheme: tabBarTheme,
tabs: _sizedTabs,
isScrollable: true,
));
......@@ -183,12 +257,12 @@ void main() {
unselectedLabelStyle: unselectedLabelStyle,
);
await tester.pumpWidget(_withTheme(tabBarTheme));
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
expect(selectedRenderObject.text.style!.fontFamily, equals(labelStyle.fontFamily));
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
expect(unselectedRenderObject.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily));
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
expect(selectedLabel.text.style!.fontFamily, equals(labelStyle.fontFamily));
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
expect(unselectedLabel.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily));
});
testWidgets('Tab bar theme with just label style specified', (WidgetTester tester) async {
......@@ -198,14 +272,14 @@ void main() {
labelStyle: labelStyle,
);
await tester.pumpWidget(_withTheme(tabBarTheme));
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
expect(selectedRenderObject.text.style!.fontFamily, equals(labelStyle.fontFamily));
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
expect(unselectedRenderObject.text.style!.fontFamily, equals('Roboto'));
expect(unselectedRenderObject.text.style!.fontSize, equals(14.0));
expect(unselectedRenderObject.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
expect(selectedLabel.text.style!.fontFamily, equals(labelStyle.fontFamily));
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
expect(unselectedLabel.text.style!.fontFamily, equals('Roboto'));
expect(unselectedLabel.text.style!.fontSize, equals(14.0));
expect(unselectedLabel.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
});
testWidgets('Tab bar label styles override theme label styles', (WidgetTester tester) async {
......@@ -221,7 +295,8 @@ void main() {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(tabBarTheme: tabBarTheme),
home: Scaffold(body: TabBar(
home: Scaffold(
body: TabBar(
tabs: _tabs,
controller: TabController(length: _tabs.length, vsync: const TestVSync()),
labelStyle: labelStyle,
......@@ -231,10 +306,10 @@ void main() {
),
);
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
expect(selectedRenderObject.text.style!.fontFamily, equals(labelStyle.fontFamily));
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
expect(unselectedRenderObject.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily));
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
expect(selectedLabel.text.style!.fontFamily, equals(labelStyle.fontFamily));
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
expect(unselectedLabel.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily));
});
testWidgets('Tab bar label padding overrides theme label padding', (WidgetTester tester) async {
......@@ -295,16 +370,25 @@ void main() {
const Color unselectedLabelColor = Colors.black;
const TabBarTheme tabBarTheme = TabBarTheme(unselectedLabelColor: unselectedLabelColor);
await tester.pumpWidget(_withTheme(tabBarTheme));
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
final RenderParagraph textRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
expect(textRenderObject.text.style!.color, equals(unselectedLabelColor));
final RenderParagraph iconRenderObject = _iconRenderObject(tester, Icons.looks_two);
final RenderParagraph iconRenderObject = _getIcon(tester, Icons.looks_two);
expect(iconRenderObject.text.style!.color, equals(unselectedLabelColor));
});
testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async {
await tester.pumpWidget(_withTheme(null, useMaterial3: true, isScrollable: true));
await tester.pumpWidget(buildTabBar(useMaterial3: true, isScrollable: true));
await expectLater(
find.byKey(_painterKey),
matchesGoldenFile('tab_bar.default.tab_indicator_size.png'),
);
});
testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async {
await tester.pumpWidget(buildTabBar(useMaterial3: true, isScrollable: true));
await expectLater(
find.byKey(_painterKey),
......@@ -315,7 +399,7 @@ void main() {
testWidgets('Tab bar theme overrides tab indicator size (tab)', (WidgetTester tester) async {
const TabBarTheme tabBarTheme = TabBarTheme(indicatorSize: TabBarIndicatorSize.tab);
await tester.pumpWidget(_withTheme(tabBarTheme));
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
await expectLater(
find.byKey(_painterKey),
......@@ -326,7 +410,7 @@ void main() {
testWidgets('Tab bar theme overrides tab indicator size (label)', (WidgetTester tester) async {
const TabBarTheme tabBarTheme = TabBarTheme(indicatorSize: TabBarIndicatorSize.label);
await tester.pumpWidget(_withTheme(tabBarTheme));
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
await expectLater(
find.byKey(_painterKey),
......@@ -337,7 +421,7 @@ void main() {
testWidgets('Tab bar theme overrides tab mouse cursor', (WidgetTester tester) async {
const TabBarTheme tabBarTheme = TabBarTheme(mouseCursor: MaterialStateMouseCursor.textable);
await tester.pumpWidget(_withTheme(tabBarTheme));
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
final Offset tabBar = tester.getCenter(
find.ancestor(of: find.text('tab 1'),matching: find.byType(TabBar)),
......@@ -356,7 +440,7 @@ void main() {
),
);
await tester.pumpWidget(_withTheme(tabBarTheme));
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
await expectLater(
find.byKey(_painterKey),
......@@ -372,7 +456,7 @@ void main() {
),
);
await tester.pumpWidget(_withTheme(tabBarTheme));
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
await expectLater(
find.byKey(_painterKey),
......@@ -386,19 +470,19 @@ void main() {
testWidgets('Tab bar defaults', (WidgetTester tester) async {
// tests for the default label color and label styles when tabBarTheme and tabBar do not provide any
await tester.pumpWidget(_withTheme(null));
await tester.pumpWidget(buildTabBar());
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
expect(selectedRenderObject.text.style!.fontFamily, equals('Roboto'));
expect(selectedRenderObject.text.style!.fontSize, equals(14.0));
expect(selectedRenderObject.text.style!.color, equals(Colors.white));
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
expect(unselectedRenderObject.text.style!.fontFamily, equals('Roboto'));
expect(unselectedRenderObject.text.style!.fontSize, equals(14.0));
expect(unselectedRenderObject.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
expect(selectedLabel.text.style!.fontFamily, equals('Roboto'));
expect(selectedLabel.text.style!.fontSize, equals(14.0));
expect(selectedLabel.text.style!.color, equals(Colors.white));
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
expect(unselectedLabel.text.style!.fontFamily, equals('Roboto'));
expect(unselectedLabel.text.style!.fontSize, equals(14.0));
expect(unselectedLabel.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
// tests for the default value of labelPadding when tabBarTheme and tabBar do not provide one
await tester.pumpWidget(_withTheme(null, tabs: _sizedTabs, isScrollable: true));
await tester.pumpWidget(buildTabBar(tabs: _sizedTabs, isScrollable: true));
const double indicatorWeight = 2.0;
final Rect tabBar = tester.getRect(find.byType(TabBar));
......@@ -423,7 +507,7 @@ void main() {
});
testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async {
await tester.pumpWidget(_withTheme(null));
await tester.pumpWidget(buildTabBar());
await expectLater(
find.byKey(_painterKey),
......
......@@ -106,6 +106,7 @@ class _NestedTabBarContainer extends StatelessWidget {
Widget buildFrame({
Key? tabBarKey,
bool secondaryTabBar = false,
required List<String> tabs,
required String value,
bool isScrollable = false,
......@@ -114,6 +115,24 @@ Widget buildFrame({
EdgeInsetsGeometry? padding,
TextDirection textDirection = TextDirection.ltr,
}) {
if (secondaryTabBar) {
return boilerplate(
textDirection: textDirection,
child: DefaultTabController(
animationDuration: animationDuration,
initialIndex: tabs.indexOf(value),
length: tabs.length,
child: TabBar.secondary(
key: tabBarKey,
tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(),
isScrollable: isScrollable,
indicatorColor: indicatorColor,
padding: padding,
),
),
);
}
return boilerplate(
textDirection: textDirection,
child: DefaultTabController(
......@@ -238,6 +257,10 @@ class TestScrollPhysics extends ScrollPhysics {
SpringDescription get spring => _kDefaultSpring;
}
RenderParagraph _getText(WidgetTester tester, String text) {
return tester.renderObject<RenderParagraph>(find.text(text));
}
void main() {
setUp(() {
debugResetSemanticsIdCounter();
......@@ -358,12 +381,12 @@ void main() {
expect(find.byType(TabBar), paints..line(color: Colors.blue[500]));
});
testWidgets('TabBar default selected/unselected text style', (WidgetTester tester) async {
testWidgets('TabBar default selected/unselected label style (primary)', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
final List<String> tabs = <String>['A', 'B', 'C'];
const String selectedValue = 'A';
const String unSelectedValue = 'C';
const String unselectedValue = 'C';
await tester.pumpWidget(
Theme(
data: theme,
......@@ -375,20 +398,95 @@ void main() {
expect(find.text('C'), findsOneWidget);
// Test selected label text style.
expect(tester.renderObject<RenderParagraph>(find.text(selectedValue)).text.style!.fontFamily, 'Roboto');
expect(tester.renderObject<RenderParagraph>(find.text(selectedValue)).text.style!.fontSize, 14.0);
expect(tester.renderObject<RenderParagraph>(
find.text(selectedValue)).text.style!.color,
theme.colorScheme.primary,
final RenderParagraph selectedLabel = _getText(tester, selectedValue);
expect(selectedLabel.text.style!.fontFamily, 'Roboto');
expect(selectedLabel.text.style!.fontSize, 14.0);
expect(selectedLabel.text.style!.color, theme.colorScheme.primary);
// Test unselected label text style.
final RenderParagraph unselectedLabel = _getText(tester, unselectedValue);
expect(unselectedLabel.text.style!.fontFamily, 'Roboto');
expect(unselectedLabel.text.style!.fontSize, 14.0);
expect(unselectedLabel.text.style!.color, theme.colorScheme.onSurfaceVariant);
});
testWidgets('TabBar default selected/unselected label style (secondary)', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
final List<String> tabs = <String>['A', 'B', 'C'];
const String selectedValue = 'A';
const String unselectedValue = 'C';
await tester.pumpWidget(
Theme(
data: theme,
child: buildFrame(tabs: tabs, value: selectedValue, secondaryTabBar: true),
),
);
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsOneWidget);
expect(find.text('C'), findsOneWidget);
// Test selected label text style.
final RenderParagraph selectedLabel = _getText(tester, selectedValue);
expect(selectedLabel.text.style!.fontFamily, 'Roboto');
expect(selectedLabel.text.style!.fontSize, 14.0);
expect(selectedLabel.text.style!.color, theme.colorScheme.onSurface);
// Test unselected label text style.
expect(tester.renderObject<RenderParagraph>(find.text(unSelectedValue)).text.style!.fontFamily, 'Roboto');
expect(tester.renderObject<RenderParagraph>(find.text(unSelectedValue)).text.style!.fontSize, 14.0);
expect(tester.renderObject<RenderParagraph>(
find.text(unSelectedValue)).text.style!.color,
theme.colorScheme.onSurfaceVariant,
final RenderParagraph unselectedLabel = _getText(tester, unselectedValue);
expect(unselectedLabel.text.style!.fontFamily, 'Roboto');
expect(unselectedLabel.text.style!.fontSize, 14.0);
expect(unselectedLabel.text.style!.color, theme.colorScheme.onSurfaceVariant);
});
testWidgets('TabBar default overlay (primary)', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
final List<String> tabs = <String>['A', 'B'];
const String selectedValue = 'A';
const String unselectedValue = 'B';
await tester.pumpWidget(
Theme(
data: theme,
child: buildFrame(tabs: tabs, value: selectedValue),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.text(selectedValue)));
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: theme.colorScheme.primary.withOpacity(0.08)));
await gesture.moveTo(tester.getCenter(find.text(unselectedValue)));
await tester.pumpAndSettle();
expect(inkFeatures, paints..rect(color: theme.colorScheme.onSurface.withOpacity(0.08)));
});
testWidgets('TabBar default overlay (secondary)', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
final List<String> tabs = <String>['A', 'B'];
const String selectedValue = 'A';
const String unselectedValue = 'B';
await tester.pumpWidget(
Theme(
data: theme,
child: buildFrame(tabs: tabs, value: selectedValue, secondaryTabBar: true),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.text(selectedValue)));
await tester.pumpAndSettle();
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
expect(inkFeatures, paints..rect(color: theme.colorScheme.onSurface.withOpacity(0.08)));
await gesture.moveTo(tester.getCenter(find.text(unselectedValue)));
await tester.pumpAndSettle();
expect(inkFeatures, paints..rect(color: theme.colorScheme.onSurface.withOpacity(0.08)));
});
testWidgets('TabBar tap selects tab', (WidgetTester tester) async {
......
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