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 { ...@@ -12,8 +12,8 @@ class TabsTemplate extends TokenTemplate {
@override @override
String generate() => ''' String generate() => '''
class _${blockName}DefaultsM3 extends TabBarTheme { class _${blockName}PrimaryDefaultsM3 extends TabBarTheme {
_${blockName}DefaultsM3(this.context) _${blockName}PrimaryDefaultsM3(this.context)
: super(indicatorSize: TabBarIndicatorSize.label); : super(indicatorSize: TabBarIndicatorSize.label);
final BuildContext context; final BuildContext context;
...@@ -69,5 +69,64 @@ class _${blockName}DefaultsM3 extends TabBarTheme { ...@@ -69,5 +69,64 @@ class _${blockName}DefaultsM3 extends TabBarTheme {
@override @override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; 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 @@ ...@@ -6,24 +6,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
void main() => runApp(const MyApp()); void main() => runApp(const TabBarApp());
class MyApp extends StatelessWidget { class TabBarApp extends StatelessWidget {
const MyApp({super.key}); const TabBarApp({super.key});
static const String _title = 'Flutter Code Sample';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const MaterialApp( return MaterialApp(
title: _title, theme: ThemeData(useMaterial3: true),
home: MyStatelessWidget(), home: const TabBarExample(),
); );
} }
} }
class MyStatelessWidget extends StatelessWidget { class TabBarExample extends StatelessWidget {
const MyStatelessWidget({super.key}); const TabBarExample({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -32,7 +30,7 @@ class MyStatelessWidget extends StatelessWidget { ...@@ -32,7 +30,7 @@ class MyStatelessWidget extends StatelessWidget {
length: 3, length: 3,
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('TabBar Widget'), title: const Text('TabBar Sample'),
bottom: const TabBar( bottom: const TabBar(
tabs: <Widget>[ tabs: <Widget>[
Tab( Tab(
......
...@@ -6,34 +6,31 @@ ...@@ -6,34 +6,31 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
void main() => runApp(const MyApp()); void main() => runApp(const TabBarApp());
class MyApp extends StatelessWidget { class TabBarApp extends StatelessWidget {
const MyApp({super.key}); const TabBarApp({super.key});
static const String _title = 'Flutter Code Sample';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const MaterialApp( return MaterialApp(
title: _title, theme: ThemeData(useMaterial3: true),
home: MyStatefulWidget(), home: const TabBarExample(),
); );
} }
} }
class MyStatefulWidget extends StatefulWidget { class TabBarExample extends StatefulWidget {
const MyStatefulWidget({super.key}); const TabBarExample({super.key});
@override @override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState(); State<TabBarExample> createState() => _TabBarExampleState();
} }
/// [AnimationController]s can be created with `vsync: this` because of /// [AnimationController]s can be created with `vsync: this` because of
/// [TickerProviderStateMixin]. /// [TickerProviderStateMixin].
class _MyStatefulWidgetState extends State<MyStatefulWidget> class _TabBarExampleState extends State<TabBarExample> with TickerProviderStateMixin {
with TickerProviderStateMixin { late final TabController _tabController;
late TabController _tabController;
@override @override
void initState() { void initState() {
...@@ -41,11 +38,17 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> ...@@ -41,11 +38,17 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget>
_tabController = TabController(length: 3, vsync: this); _tabController = TabController(length: 3, vsync: this);
} }
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('TabBar Widget'), title: const Text('TabBar Sample'),
bottom: TabBar( bottom: TabBar(
controller: _tabController, controller: _tabController,
tabs: const <Widget>[ 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 { ...@@ -167,24 +167,27 @@ class _TabStyle extends AnimatedWidget {
const _TabStyle({ const _TabStyle({
required Animation<double> animation, required Animation<double> animation,
required this.isSelected, required this.isSelected,
required this.isPrimary,
required this.labelColor, required this.labelColor,
required this.unselectedLabelColor, required this.unselectedLabelColor,
required this.labelStyle, required this.labelStyle,
required this.unselectedLabelStyle, required this.unselectedLabelStyle,
required this.defaults,
required this.child, required this.child,
}) : super(listenable: animation); }) : super(listenable: animation);
final TextStyle? labelStyle; final TextStyle? labelStyle;
final TextStyle? unselectedLabelStyle; final TextStyle? unselectedLabelStyle;
final bool isSelected; final bool isSelected;
final bool isPrimary;
final Color? labelColor; final Color? labelColor;
final Color? unselectedLabelColor; final Color? unselectedLabelColor;
final TabBarTheme defaults;
final Widget child; final Widget child;
MaterialStateColor _resolveWithLabelColor(BuildContext context) { MaterialStateColor _resolveWithLabelColor(BuildContext context) {
final ThemeData themeData = Theme.of(context); final ThemeData themeData = Theme.of(context);
final TabBarTheme tabBarTheme = TabBarTheme.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 Animation<double> animation = listenable as Animation<double>;
// labelStyle.color (and tabBarTheme.labelStyle.color) is not considered // labelStyle.color (and tabBarTheme.labelStyle.color) is not considered
...@@ -219,9 +222,7 @@ class _TabStyle extends AnimatedWidget { ...@@ -219,9 +222,7 @@ class _TabStyle extends AnimatedWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final TabBarTheme tabBarTheme = TabBarTheme.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 Animation<double> animation = listenable as Animation<double>;
final Set<MaterialState> states = isSelected final Set<MaterialState> states = isSelected
...@@ -604,7 +605,10 @@ class _TabBarScrollController extends ScrollController { ...@@ -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 /// Typically created as the [AppBar.bottom] part of an [AppBar] and in
/// conjunction with a [TabBarView]. /// conjunction with a [TabBarView].
...@@ -635,12 +639,23 @@ class _TabBarScrollController extends ScrollController { ...@@ -635,12 +639,23 @@ class _TabBarScrollController extends ScrollController {
/// ** See code in examples/api/lib/material/tabs/tab_bar.1.dart ** /// ** See code in examples/api/lib/material/tabs/tab_bar.1.dart **
/// {@end-tool} /// {@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: /// See also:
/// ///
/// * [TabBar.secondary], for a secondary tab bar.
/// * [TabBarView], which displays page views that correspond to each tab. /// * [TabBarView], which displays page views that correspond to each tab.
/// * [TabController], which coordinates tab selection between a [TabBar] and a [TabBarView]. /// * [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 { 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 /// The [tabs] argument must not be null and its length must match the [controller]'s
/// [TabController.length]. /// [TabController.length].
...@@ -680,7 +695,57 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -680,7 +695,57 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.physics, this.physics,
this.splashFactory, this.splashFactory,
this.splashBorderRadius, 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. /// Typically a list of two or more [Tab] widgets.
/// ///
...@@ -993,6 +1058,11 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -993,6 +1058,11 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
return false; return false;
} }
/// Whether this tab bar is a primary tab bar.
///
/// Otherwise, it is a secondary tab bar.
final bool _isPrimary;
@override @override
State<TabBar> createState() => _TabBarState(); State<TabBar> createState() => _TabBarState();
} }
...@@ -1016,10 +1086,19 @@ class _TabBarState extends State<TabBar> { ...@@ -1016,10 +1086,19 @@ class _TabBarState extends State<TabBar> {
_labelPaddings = List<EdgeInsetsGeometry>.filled(widget.tabs.length, EdgeInsets.zero, growable: true); _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() { Decoration _getIndicator() {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final TabBarTheme tabBarTheme = TabBarTheme.of(context); final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final TabBarTheme defaults = theme.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
if (widget.indicator != null) { if (widget.indicator != null) {
return widget.indicator!; return widget.indicator!;
...@@ -1030,7 +1109,7 @@ class _TabBarState extends State<TabBar> { ...@@ -1030,7 +1109,7 @@ class _TabBarState extends State<TabBar> {
Color color = widget.indicatorColor Color color = widget.indicatorColor
?? (theme.useMaterial3 ?? (theme.useMaterial3
? tabBarTheme.indicatorColor ?? defaults.indicatorColor! ? tabBarTheme.indicatorColor ?? _defaults.indicatorColor!
: Theme.of(context).indicatorColor); : Theme.of(context).indicatorColor);
// ThemeData tries to avoid this by having indicatorColor avoid being the // ThemeData tries to avoid this by having indicatorColor avoid being the
// primaryColor. However, it's possible that the tab bar is on a // primaryColor. However, it's possible that the tab bar is on a
...@@ -1046,12 +1125,13 @@ class _TabBarState extends State<TabBar> { ...@@ -1046,12 +1125,13 @@ class _TabBarState extends State<TabBar> {
// TODO(xu-baolin): Remove automatic adjustment to white color indicator // TODO(xu-baolin): Remove automatic adjustment to white color indicator
// with a better long-term solution. // with a better long-term solution.
// https://github.com/flutter/flutter/pull/68171#pullrequestreview-517753917 // 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; color = Colors.white;
} }
return UnderlineTabIndicator( return UnderlineTabIndicator(
borderRadius: theme.useMaterial3 borderRadius: theme.useMaterial3 && widget._isPrimary
// TODO(tahatesser): Make sure this value matches Material 3 Tabs spec // TODO(tahatesser): Make sure this value matches Material 3 Tabs spec
// when `preferredSize`and `indicatorWeight` are updated to support Material 3 // when `preferredSize`and `indicatorWeight` are updated to support Material 3
// https://m3.material.io/components/tabs/specs#149a189f-9039-4195-99da-15c205d20e30, // https://m3.material.io/components/tabs/specs#149a189f-9039-4195-99da-15c205d20e30,
...@@ -1107,16 +1187,15 @@ class _TabBarState extends State<TabBar> { ...@@ -1107,16 +1187,15 @@ class _TabBarState extends State<TabBar> {
void _initIndicatorPainter() { void _initIndicatorPainter() {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final TabBarTheme tabBarTheme = TabBarTheme.of(context); final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final TabBarTheme defaults = theme.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
_indicatorPainter = !_controllerIsValid ? null : _IndicatorPainter( _indicatorPainter = !_controllerIsValid ? null : _IndicatorPainter(
controller: _controller!, controller: _controller!,
indicator: _getIndicator(), indicator: _getIndicator(),
indicatorSize: widget.indicatorSize ?? tabBarTheme.indicatorSize ?? defaults.indicatorSize!, indicatorSize: widget.indicatorSize ?? tabBarTheme.indicatorSize ?? _defaults.indicatorSize!,
indicatorPadding: widget.indicatorPadding, indicatorPadding: widget.indicatorPadding,
tabKeys: _tabKeys, tabKeys: _tabKeys,
old: _indicatorPainter, old: _indicatorPainter,
dividerColor: theme.useMaterial3 ? widget.dividerColor ?? tabBarTheme.dividerColor ?? defaults.dividerColor : null, dividerColor: theme.useMaterial3 ? widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor : null,
labelPaddings: _labelPaddings, labelPaddings: _labelPaddings,
); );
} }
...@@ -1262,14 +1341,16 @@ class _TabBarState extends State<TabBar> { ...@@ -1262,14 +1341,16 @@ class _TabBarState extends State<TabBar> {
widget.onTap?.call(index); 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( return _TabStyle(
animation: animation, animation: animation,
isSelected: isSelected, isSelected: isSelected,
isPrimary: widget._isPrimary,
labelColor: widget.labelColor, labelColor: widget.labelColor,
unselectedLabelColor: widget.unselectedLabelColor, unselectedLabelColor: widget.unselectedLabelColor,
labelStyle: widget.labelStyle, labelStyle: widget.labelStyle,
unselectedLabelStyle: widget.unselectedLabelStyle, unselectedLabelStyle: widget.unselectedLabelStyle,
defaults: defaults,
child: child, child: child,
); );
} }
...@@ -1309,9 +1390,7 @@ class _TabBarState extends State<TabBar> { ...@@ -1309,9 +1390,7 @@ class _TabBarState extends State<TabBar> {
); );
} }
final ThemeData theme = Theme.of(context);
final TabBarTheme tabBarTheme = TabBarTheme.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) { final List<Widget> wrappedTabs = List<Widget>.generate(widget.tabs.length, (int index) {
const double verticalAdjustment = (_kTextAndIconTabHeight - _kTabHeight)/2.0; const double verticalAdjustment = (_kTextAndIconTabHeight - _kTabHeight)/2.0;
...@@ -1353,22 +1432,22 @@ class _TabBarState extends State<TabBar> { ...@@ -1353,22 +1432,22 @@ class _TabBarState extends State<TabBar> {
// The user tapped on a tab, the tab controller's animation is running. // The user tapped on a tab, the tab controller's animation is running.
assert(_currentIndex != previousIndex); assert(_currentIndex != previousIndex);
final Animation<double> animation = _ChangeAnimation(_controller!); final Animation<double> animation = _ChangeAnimation(_controller!);
wrappedTabs[_currentIndex!] = _buildStyledTab(wrappedTabs[_currentIndex!], true, animation); wrappedTabs[_currentIndex!] = _buildStyledTab(wrappedTabs[_currentIndex!], true, animation, _defaults);
wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation); wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation, _defaults);
} else { } else {
// The user is dragging the TabBarView's PageView left or right. // The user is dragging the TabBarView's PageView left or right.
final int tabIndex = _currentIndex!; final int tabIndex = _currentIndex!;
final Animation<double> centerAnimation = _DragAnimation(_controller!, tabIndex); 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) { if (_currentIndex! > 0) {
final int tabIndex = _currentIndex! - 1; final int tabIndex = _currentIndex! - 1;
final Animation<double> previousAnimation = ReverseAnimation(_DragAnimation(_controller!, tabIndex)); 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) { if (_currentIndex! < widget.tabs.length - 1) {
final int tabIndex = _currentIndex! + 1; final int tabIndex = _currentIndex! + 1;
final Animation<double> nextAnimation = ReverseAnimation(_DragAnimation(_controller!, tabIndex)); 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> { ...@@ -1389,7 +1468,7 @@ class _TabBarState extends State<TabBar> {
final MaterialStateProperty<Color?> defaultOverlay = MaterialStateProperty.resolveWith<Color?>( final MaterialStateProperty<Color?> defaultOverlay = MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) { (Set<MaterialState> states) {
final Set<MaterialState> effectiveStates = selectedState..addAll(states); final Set<MaterialState> effectiveStates = selectedState..addAll(states);
return defaults.overlayColor?.resolve(effectiveStates); return _defaults.overlayColor?.resolve(effectiveStates);
}, },
); );
wrappedTabs[index] = InkWell( wrappedTabs[index] = InkWell(
...@@ -1397,7 +1476,7 @@ class _TabBarState extends State<TabBar> { ...@@ -1397,7 +1476,7 @@ class _TabBarState extends State<TabBar> {
onTap: () { _handleTap(index); }, onTap: () { _handleTap(index); },
enableFeedback: widget.enableFeedback ?? true, enableFeedback: widget.enableFeedback ?? true,
overlayColor: widget.overlayColor ?? tabBarTheme.overlayColor ?? defaultOverlay, overlayColor: widget.overlayColor ?? tabBarTheme.overlayColor ?? defaultOverlay,
splashFactory: widget.splashFactory ?? tabBarTheme.splashFactory ?? defaults.splashFactory, splashFactory: widget.splashFactory ?? tabBarTheme.splashFactory ?? _defaults.splashFactory,
borderRadius: widget.splashBorderRadius, borderRadius: widget.splashBorderRadius,
child: Padding( child: Padding(
padding: EdgeInsets.only(bottom: widget.indicatorWeight), padding: EdgeInsets.only(bottom: widget.indicatorWeight),
...@@ -1422,10 +1501,12 @@ class _TabBarState extends State<TabBar> { ...@@ -1422,10 +1501,12 @@ class _TabBarState extends State<TabBar> {
child: _TabStyle( child: _TabStyle(
animation: kAlwaysDismissedAnimation, animation: kAlwaysDismissedAnimation,
isSelected: false, isSelected: false,
isPrimary: widget._isPrimary,
labelColor: widget.labelColor, labelColor: widget.labelColor,
unselectedLabelColor: widget.unselectedLabelColor, unselectedLabelColor: widget.unselectedLabelColor,
labelStyle: widget.labelStyle, labelStyle: widget.labelStyle,
unselectedLabelStyle: widget.unselectedLabelStyle, unselectedLabelStyle: widget.unselectedLabelStyle,
defaults: _defaults,
child: _TabLabelBar( child: _TabLabelBar(
onPerformLayout: _saveTabOffsets, onPerformLayout: _saveTabOffsets,
children: wrappedTabs, children: wrappedTabs,
...@@ -1979,8 +2060,8 @@ class _TabsDefaultsM2 extends TabBarTheme { ...@@ -1979,8 +2060,8 @@ class _TabsDefaultsM2 extends TabBarTheme {
// Token database version: v0_162 // Token database version: v0_162
class _TabsDefaultsM3 extends TabBarTheme { class _TabsPrimaryDefaultsM3 extends TabBarTheme {
_TabsDefaultsM3(this.context) _TabsPrimaryDefaultsM3(this.context)
: super(indicatorSize: TabBarIndicatorSize.label); : super(indicatorSize: TabBarIndicatorSize.label);
final BuildContext context; final BuildContext context;
...@@ -2037,4 +2118,62 @@ class _TabsDefaultsM3 extends TabBarTheme { ...@@ -2037,4 +2118,62 @@ class _TabsDefaultsM3 extends TabBarTheme {
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory; 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 // END GENERATED TOKEN PROPERTIES - Tabs
...@@ -31,14 +31,30 @@ final List<SizedBox> _sizedTabs = <SizedBox>[ ...@@ -31,14 +31,30 @@ final List<SizedBox> _sizedTabs = <SizedBox>[
SizedBox(key: UniqueKey(), width: 100.0, height: 50.0), SizedBox(key: UniqueKey(), width: 100.0, height: 50.0),
]; ];
Widget _withTheme( Widget buildTabBar({
TabBarTheme? theme, { TabBarTheme? tabBarTheme,
bool secondaryTabBar = false,
List<Widget> tabs = _tabs, List<Widget> tabs = _tabs,
bool isScrollable = false, bool isScrollable = false,
bool useMaterial3 = false, bool useMaterial3 = false,
}) { }) {
if (secondaryTabBar) {
return MaterialApp( 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( home: Scaffold(
body: RepaintBoundary( body: RepaintBoundary(
key: _painterKey, key: _painterKey,
...@@ -52,12 +68,17 @@ Widget _withTheme( ...@@ -52,12 +68,17 @@ Widget _withTheme(
); );
} }
RenderParagraph _iconRenderObject(WidgetTester tester, IconData icon) {
RenderParagraph _getIcon(WidgetTester tester, IconData icon) {
return tester.renderObject<RenderParagraph>( return tester.renderObject<RenderParagraph>(
find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)), 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() { void main() {
test('TabBarTheme copyWith, ==, hashCode, defaults', () { test('TabBarTheme copyWith, ==, hashCode, defaults', () {
expect(const TabBarTheme(), const TabBarTheme().copyWith()); expect(const TabBarTheme(), const TabBarTheme().copyWith());
...@@ -82,60 +103,113 @@ void main() { ...@@ -82,60 +103,113 @@ void main() {
expect(identical(TabBarTheme.lerp(theme, theme, 0.5), theme), true); expect(identical(TabBarTheme.lerp(theme, theme, 0.5), theme), true);
}); });
testWidgets('Tab bar defaults', (WidgetTester tester) async { testWidgets('Tab bar defaults (primary)', (WidgetTester tester) async {
// tests for the default label color and label styles when tabBarTheme and tabBar do not provide any // Test default label color and label styles.
await tester.pumpWidget(_withTheme(null, useMaterial3: true)); await tester.pumpWidget(buildTabBar(useMaterial3: true));
final ThemeData theme = ThemeData(useMaterial3: true); final ThemeData theme = ThemeData(useMaterial3: true);
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text)); final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
expect(selectedRenderObject.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily)); expect(selectedLabel.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
expect(selectedRenderObject.text.style!.fontSize, equals(14.0)); expect(selectedLabel.text.style!.fontSize, equals(14.0));
expect(selectedRenderObject.text.style!.color, equals(theme.colorScheme.primary)); expect(selectedLabel.text.style!.color, equals(theme.colorScheme.primary));
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text)); final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
expect(unselectedRenderObject.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily)); expect(unselectedLabel.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
expect(unselectedRenderObject.text.style!.fontSize, equals(14.0)); expect(unselectedLabel.text.style!.fontSize, equals(14.0));
expect(unselectedRenderObject.text.style!.color, equals(theme.colorScheme.onSurfaceVariant)); expect(unselectedLabel.text.style!.color, equals(theme.colorScheme.onSurfaceVariant));
// tests for the default value of labelPadding when tabBarTheme and tabBar do not provide one // Test default labelPadding.
await tester.pumpWidget(_withTheme(null, tabs: _sizedTabs, isScrollable: true)); await tester.pumpWidget(buildTabBar(tabs: _sizedTabs, isScrollable: true));
const double indicatorWeight = 2.0; const double indicatorWeight = 2.0;
final Rect tabBar = tester.getRect(find.byType(TabBar)); final Rect tabBar = tester.getRect(find.byType(TabBar));
final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!)); final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!));
final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].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.left, equals(kTabLabelPadding.left));
expect(tabOneRect.top, equals(kTabLabelPadding.top)); expect(tabOneRect.top, equals(kTabLabelPadding.top));
expect(tabOneRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight)); 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.right, equals(tabBar.width - kTabLabelPadding.right));
expect(tabTwoRect.top, equals(kTabLabelPadding.top)); expect(tabTwoRect.top, equals(kTabLabelPadding.top));
expect(tabTwoRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight)); 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)); 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)); final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect( expect(
tabBarBox, tabBarBox,
paints paints
..line(color: theme.colorScheme.surfaceVariant) ..line(color: theme.colorScheme.surfaceVariant)
// Indicator is a rrect in the primary tab bar.
..rrect(color: theme.colorScheme.primary), ..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 { testWidgets('Tab bar theme overrides label color (selected)', (WidgetTester tester) async {
const Color labelColor = Colors.black; const Color labelColor = Colors.black;
const TabBarTheme tabBarTheme = TabBarTheme(labelColor: labelColor); 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)); final RenderParagraph tabLabel = _getText(tester, _tab1Text);
expect(textRenderObject.text.style!.color, equals(labelColor)); expect(tabLabel.text.style!.color, equals(labelColor));
final RenderParagraph iconRenderObject = _iconRenderObject(tester, Icons.looks_one); final RenderParagraph tabIcon = _getIcon(tester, Icons.looks_one);
expect(iconRenderObject.text.style!.color, equals(labelColor)); expect(tabIcon.text.style!.color, equals(labelColor));
}); });
testWidgets('Tab bar theme overrides label padding', (WidgetTester tester) async { testWidgets('Tab bar theme overrides label padding', (WidgetTester tester) async {
...@@ -151,8 +225,8 @@ void main() { ...@@ -151,8 +225,8 @@ void main() {
const TabBarTheme tabBarTheme = TabBarTheme(labelPadding: labelPadding); const TabBarTheme tabBarTheme = TabBarTheme(labelPadding: labelPadding);
await tester.pumpWidget(_withTheme( await tester.pumpWidget(buildTabBar(
tabBarTheme, tabBarTheme: tabBarTheme,
tabs: _sizedTabs, tabs: _sizedTabs,
isScrollable: true, isScrollable: true,
)); ));
...@@ -183,12 +257,12 @@ void main() { ...@@ -183,12 +257,12 @@ void main() {
unselectedLabelStyle: unselectedLabelStyle, unselectedLabelStyle: unselectedLabelStyle,
); );
await tester.pumpWidget(_withTheme(tabBarTheme)); await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text)); final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
expect(selectedRenderObject.text.style!.fontFamily, equals(labelStyle.fontFamily)); expect(selectedLabel.text.style!.fontFamily, equals(labelStyle.fontFamily));
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text)); final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
expect(unselectedRenderObject.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily)); expect(unselectedLabel.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily));
}); });
testWidgets('Tab bar theme with just label style specified', (WidgetTester tester) async { testWidgets('Tab bar theme with just label style specified', (WidgetTester tester) async {
...@@ -198,14 +272,14 @@ void main() { ...@@ -198,14 +272,14 @@ void main() {
labelStyle: labelStyle, labelStyle: labelStyle,
); );
await tester.pumpWidget(_withTheme(tabBarTheme)); await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text)); final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
expect(selectedRenderObject.text.style!.fontFamily, equals(labelStyle.fontFamily)); expect(selectedLabel.text.style!.fontFamily, equals(labelStyle.fontFamily));
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text)); final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
expect(unselectedRenderObject.text.style!.fontFamily, equals('Roboto')); expect(unselectedLabel.text.style!.fontFamily, equals('Roboto'));
expect(unselectedRenderObject.text.style!.fontSize, equals(14.0)); expect(unselectedLabel.text.style!.fontSize, equals(14.0));
expect(unselectedRenderObject.text.style!.color, equals(Colors.white.withAlpha(0xB2))); expect(unselectedLabel.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
}); });
testWidgets('Tab bar label styles override theme label styles', (WidgetTester tester) async { testWidgets('Tab bar label styles override theme label styles', (WidgetTester tester) async {
...@@ -221,7 +295,8 @@ void main() { ...@@ -221,7 +295,8 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: ThemeData(tabBarTheme: tabBarTheme), theme: ThemeData(tabBarTheme: tabBarTheme),
home: Scaffold(body: TabBar( home: Scaffold(
body: TabBar(
tabs: _tabs, tabs: _tabs,
controller: TabController(length: _tabs.length, vsync: const TestVSync()), controller: TabController(length: _tabs.length, vsync: const TestVSync()),
labelStyle: labelStyle, labelStyle: labelStyle,
...@@ -231,10 +306,10 @@ void main() { ...@@ -231,10 +306,10 @@ void main() {
), ),
); );
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text)); final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
expect(selectedRenderObject.text.style!.fontFamily, equals(labelStyle.fontFamily)); expect(selectedLabel.text.style!.fontFamily, equals(labelStyle.fontFamily));
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text)); final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
expect(unselectedRenderObject.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily)); expect(unselectedLabel.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily));
}); });
testWidgets('Tab bar label padding overrides theme label padding', (WidgetTester tester) async { testWidgets('Tab bar label padding overrides theme label padding', (WidgetTester tester) async {
...@@ -295,16 +370,25 @@ void main() { ...@@ -295,16 +370,25 @@ void main() {
const Color unselectedLabelColor = Colors.black; const Color unselectedLabelColor = Colors.black;
const TabBarTheme tabBarTheme = TabBarTheme(unselectedLabelColor: unselectedLabelColor); 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)); final RenderParagraph textRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
expect(textRenderObject.text.style!.color, equals(unselectedLabelColor)); 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)); expect(iconRenderObject.text.style!.color, equals(unselectedLabelColor));
}); });
testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async { 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( await expectLater(
find.byKey(_painterKey), find.byKey(_painterKey),
...@@ -315,7 +399,7 @@ void main() { ...@@ -315,7 +399,7 @@ void main() {
testWidgets('Tab bar theme overrides tab indicator size (tab)', (WidgetTester tester) async { testWidgets('Tab bar theme overrides tab indicator size (tab)', (WidgetTester tester) async {
const TabBarTheme tabBarTheme = TabBarTheme(indicatorSize: TabBarIndicatorSize.tab); const TabBarTheme tabBarTheme = TabBarTheme(indicatorSize: TabBarIndicatorSize.tab);
await tester.pumpWidget(_withTheme(tabBarTheme)); await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
await expectLater( await expectLater(
find.byKey(_painterKey), find.byKey(_painterKey),
...@@ -326,7 +410,7 @@ void main() { ...@@ -326,7 +410,7 @@ void main() {
testWidgets('Tab bar theme overrides tab indicator size (label)', (WidgetTester tester) async { testWidgets('Tab bar theme overrides tab indicator size (label)', (WidgetTester tester) async {
const TabBarTheme tabBarTheme = TabBarTheme(indicatorSize: TabBarIndicatorSize.label); const TabBarTheme tabBarTheme = TabBarTheme(indicatorSize: TabBarIndicatorSize.label);
await tester.pumpWidget(_withTheme(tabBarTheme)); await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
await expectLater( await expectLater(
find.byKey(_painterKey), find.byKey(_painterKey),
...@@ -337,7 +421,7 @@ void main() { ...@@ -337,7 +421,7 @@ void main() {
testWidgets('Tab bar theme overrides tab mouse cursor', (WidgetTester tester) async { testWidgets('Tab bar theme overrides tab mouse cursor', (WidgetTester tester) async {
const TabBarTheme tabBarTheme = TabBarTheme(mouseCursor: MaterialStateMouseCursor.textable); const TabBarTheme tabBarTheme = TabBarTheme(mouseCursor: MaterialStateMouseCursor.textable);
await tester.pumpWidget(_withTheme(tabBarTheme)); await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
final Offset tabBar = tester.getCenter( final Offset tabBar = tester.getCenter(
find.ancestor(of: find.text('tab 1'),matching: find.byType(TabBar)), find.ancestor(of: find.text('tab 1'),matching: find.byType(TabBar)),
...@@ -356,7 +440,7 @@ void main() { ...@@ -356,7 +440,7 @@ void main() {
), ),
); );
await tester.pumpWidget(_withTheme(tabBarTheme)); await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
await expectLater( await expectLater(
find.byKey(_painterKey), find.byKey(_painterKey),
...@@ -372,7 +456,7 @@ void main() { ...@@ -372,7 +456,7 @@ void main() {
), ),
); );
await tester.pumpWidget(_withTheme(tabBarTheme)); await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
await expectLater( await expectLater(
find.byKey(_painterKey), find.byKey(_painterKey),
...@@ -386,19 +470,19 @@ void main() { ...@@ -386,19 +470,19 @@ void main() {
testWidgets('Tab bar defaults', (WidgetTester tester) async { testWidgets('Tab bar defaults', (WidgetTester tester) async {
// tests for the default label color and label styles when tabBarTheme and tabBar do not provide any // 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)); final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
expect(selectedRenderObject.text.style!.fontFamily, equals('Roboto')); expect(selectedLabel.text.style!.fontFamily, equals('Roboto'));
expect(selectedRenderObject.text.style!.fontSize, equals(14.0)); expect(selectedLabel.text.style!.fontSize, equals(14.0));
expect(selectedRenderObject.text.style!.color, equals(Colors.white)); expect(selectedLabel.text.style!.color, equals(Colors.white));
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text)); final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
expect(unselectedRenderObject.text.style!.fontFamily, equals('Roboto')); expect(unselectedLabel.text.style!.fontFamily, equals('Roboto'));
expect(unselectedRenderObject.text.style!.fontSize, equals(14.0)); expect(unselectedLabel.text.style!.fontSize, equals(14.0));
expect(unselectedRenderObject.text.style!.color, equals(Colors.white.withAlpha(0xB2))); 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 // 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; const double indicatorWeight = 2.0;
final Rect tabBar = tester.getRect(find.byType(TabBar)); final Rect tabBar = tester.getRect(find.byType(TabBar));
...@@ -423,7 +507,7 @@ void main() { ...@@ -423,7 +507,7 @@ void main() {
}); });
testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async { testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async {
await tester.pumpWidget(_withTheme(null)); await tester.pumpWidget(buildTabBar());
await expectLater( await expectLater(
find.byKey(_painterKey), find.byKey(_painterKey),
......
...@@ -106,6 +106,7 @@ class _NestedTabBarContainer extends StatelessWidget { ...@@ -106,6 +106,7 @@ class _NestedTabBarContainer extends StatelessWidget {
Widget buildFrame({ Widget buildFrame({
Key? tabBarKey, Key? tabBarKey,
bool secondaryTabBar = false,
required List<String> tabs, required List<String> tabs,
required String value, required String value,
bool isScrollable = false, bool isScrollable = false,
...@@ -114,6 +115,24 @@ Widget buildFrame({ ...@@ -114,6 +115,24 @@ Widget buildFrame({
EdgeInsetsGeometry? padding, EdgeInsetsGeometry? padding,
TextDirection textDirection = TextDirection.ltr, 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( return boilerplate(
textDirection: textDirection, textDirection: textDirection,
child: DefaultTabController( child: DefaultTabController(
...@@ -238,6 +257,10 @@ class TestScrollPhysics extends ScrollPhysics { ...@@ -238,6 +257,10 @@ class TestScrollPhysics extends ScrollPhysics {
SpringDescription get spring => _kDefaultSpring; SpringDescription get spring => _kDefaultSpring;
} }
RenderParagraph _getText(WidgetTester tester, String text) {
return tester.renderObject<RenderParagraph>(find.text(text));
}
void main() { void main() {
setUp(() { setUp(() {
debugResetSemanticsIdCounter(); debugResetSemanticsIdCounter();
...@@ -358,12 +381,12 @@ void main() { ...@@ -358,12 +381,12 @@ void main() {
expect(find.byType(TabBar), paints..line(color: Colors.blue[500])); 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 ThemeData theme = ThemeData(useMaterial3: true);
final List<String> tabs = <String>['A', 'B', 'C']; final List<String> tabs = <String>['A', 'B', 'C'];
const String selectedValue = 'A'; const String selectedValue = 'A';
const String unSelectedValue = 'C'; const String unselectedValue = 'C';
await tester.pumpWidget( await tester.pumpWidget(
Theme( Theme(
data: theme, data: theme,
...@@ -375,20 +398,95 @@ void main() { ...@@ -375,20 +398,95 @@ void main() {
expect(find.text('C'), findsOneWidget); expect(find.text('C'), findsOneWidget);
// Test selected label text style. // Test selected label text style.
expect(tester.renderObject<RenderParagraph>(find.text(selectedValue)).text.style!.fontFamily, 'Roboto'); final RenderParagraph selectedLabel = _getText(tester, selectedValue);
expect(tester.renderObject<RenderParagraph>(find.text(selectedValue)).text.style!.fontSize, 14.0); expect(selectedLabel.text.style!.fontFamily, 'Roboto');
expect(tester.renderObject<RenderParagraph>( expect(selectedLabel.text.style!.fontSize, 14.0);
find.text(selectedValue)).text.style!.color, expect(selectedLabel.text.style!.color, theme.colorScheme.primary);
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. // Test unselected label text style.
expect(tester.renderObject<RenderParagraph>(find.text(unSelectedValue)).text.style!.fontFamily, 'Roboto'); final RenderParagraph unselectedLabel = _getText(tester, unselectedValue);
expect(tester.renderObject<RenderParagraph>(find.text(unSelectedValue)).text.style!.fontSize, 14.0); expect(unselectedLabel.text.style!.fontFamily, 'Roboto');
expect(tester.renderObject<RenderParagraph>( expect(unselectedLabel.text.style!.fontSize, 14.0);
find.text(unSelectedValue)).text.style!.color, expect(unselectedLabel.text.style!.color, theme.colorScheme.onSurfaceVariant);
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 { 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