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);
});
}
This diff is collapsed.
......@@ -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