Unverified Commit 0bcd228e authored by Taha Tesser's avatar Taha Tesser Committed by GitHub

Update `TabBar` and `TabBar.secondary` to use indicator height/color M3 tokens (#145753)

fixes [Secondary `TabBar` indicator height token is missing ](https://github.com/flutter/flutter/issues/124965)

### Code sample

<details>
<summary>expand to view the code sample</summary> 

```dart
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: DefaultTabController(
        length: 2,
        child: Scaffold(
          appBar: AppBar(
            title: const Text('Sample'),
            bottom: const TabBar.secondary(
              tabs: <Widget>[
                Tab(icon: Icon(Icons.directions_car)),
                Tab(icon: Icon(Icons.directions_transit)),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () {},
            child: const Icon(Icons.add),
          ),
        ),
      ),
    );
  }
}
```

</details>
parent c311d42c
...@@ -599,6 +599,8 @@ md.comp.search-view.header.input-text.color, ...@@ -599,6 +599,8 @@ md.comp.search-view.header.input-text.color,
md.comp.search-view.header.input-text.text-style, md.comp.search-view.header.input-text.text-style,
md.comp.search-view.header.supporting-text.color, md.comp.search-view.header.supporting-text.color,
md.comp.search-view.header.supporting-text.text-style, md.comp.search-view.header.supporting-text.text-style,
md.comp.secondary-navigation-tab.active-indicator.color,
md.comp.secondary-navigation-tab.active-indicator.height,
md.comp.secondary-navigation-tab.active.label-text.color, md.comp.secondary-navigation-tab.active.label-text.color,
md.comp.secondary-navigation-tab.focus.state-layer.color, md.comp.secondary-navigation-tab.focus.state-layer.color,
md.comp.secondary-navigation-tab.focus.state-layer.opacity, md.comp.secondary-navigation-tab.focus.state-layer.opacity,
......
...@@ -78,7 +78,12 @@ class _${blockName}PrimaryDefaultsM3 extends TabBarTheme { ...@@ -78,7 +78,12 @@ class _${blockName}PrimaryDefaultsM3 extends TabBarTheme {
@override @override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill; TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
static double indicatorWeight = ${getToken('md.comp.primary-navigation-tab.active-indicator.height')}; static double indicatorWeight(TabBarIndicatorSize indicatorSize) {
return switch (indicatorSize) {
TabBarIndicatorSize.label => ${getToken('md.comp.primary-navigation-tab.active-indicator.height')},
TabBarIndicatorSize.tab => ${getToken('md.comp.secondary-navigation-tab.active-indicator.height')},
};
}
// TODO(davidmartos96): This value doesn't currently exist in // TODO(davidmartos96): This value doesn't currently exist in
// https://m3.material.io/components/tabs/specs // https://m3.material.io/components/tabs/specs
...@@ -104,7 +109,7 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme { ...@@ -104,7 +109,7 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme {
double? get dividerHeight => ${getToken("md.comp.divider.thickness")}; double? get dividerHeight => ${getToken("md.comp.divider.thickness")};
@override @override
Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")}; Color? get indicatorColor => ${componentColor("md.comp.secondary-navigation-tab.active-indicator")};
@override @override
Color? get labelColor => ${componentColor("md.comp.secondary-navigation-tab.active.label-text")}; Color? get labelColor => ${componentColor("md.comp.secondary-navigation-tab.active.label-text")};
...@@ -151,6 +156,8 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme { ...@@ -151,6 +156,8 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme {
@override @override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill; TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
static double indicatorWeight = ${getToken('md.comp.secondary-navigation-tab.active-indicator.height')};
} }
'''; ''';
......
...@@ -1343,11 +1343,20 @@ class _TabBarState extends State<TabBar> { ...@@ -1343,11 +1343,20 @@ class _TabBarState extends State<TabBar> {
color = Colors.white; color = Colors.white;
} }
final bool primaryWithLabelIndicator = widget._isPrimary && indicatorSize == TabBarIndicatorSize.label; final double effectiveIndicatorWeight = theme.useMaterial3
final double effectiveIndicatorWeight = theme.useMaterial3 && primaryWithLabelIndicator ? math.max(
? math.max(widget.indicatorWeight, _TabsPrimaryDefaultsM3.indicatorWeight) widget.indicatorWeight,
switch (widget._isPrimary) {
true => _TabsPrimaryDefaultsM3.indicatorWeight(indicatorSize),
false => _TabsSecondaryDefaultsM3.indicatorWeight,
},
)
: widget.indicatorWeight; : widget.indicatorWeight;
// Only Material 3 primary TabBar with label indicatorSize should be rounded. // Only Material 3 primary TabBar with label indicatorSize should be rounded.
final bool primaryWithLabelIndicator = switch (indicatorSize) {
TabBarIndicatorSize.label => widget._isPrimary,
TabBarIndicatorSize.tab => false,
};
final BorderRadius? effectiveBorderRadius = theme.useMaterial3 && primaryWithLabelIndicator final BorderRadius? effectiveBorderRadius = theme.useMaterial3 && primaryWithLabelIndicator
? BorderRadius.only( ? BorderRadius.only(
topLeft: Radius.circular(effectiveIndicatorWeight), topLeft: Radius.circular(effectiveIndicatorWeight),
...@@ -2429,7 +2438,12 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme { ...@@ -2429,7 +2438,12 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme {
@override @override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill; TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
static double indicatorWeight = 3.0; static double indicatorWeight(TabBarIndicatorSize indicatorSize) {
return switch (indicatorSize) {
TabBarIndicatorSize.label => 3.0,
TabBarIndicatorSize.tab => 2.0,
};
}
// TODO(davidmartos96): This value doesn't currently exist in // TODO(davidmartos96): This value doesn't currently exist in
// https://m3.material.io/components/tabs/specs // https://m3.material.io/components/tabs/specs
...@@ -2502,6 +2516,8 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme { ...@@ -2502,6 +2516,8 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme {
@override @override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill; TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
static double indicatorWeight = 2.0;
} }
// END GENERATED TOKEN PROPERTIES - Tabs // END GENERATED TOKEN PROPERTIES - Tabs
...@@ -13,9 +13,15 @@ import '../widgets/semantics_tester.dart'; ...@@ -13,9 +13,15 @@ import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart'; import 'feedback_tester.dart';
import 'tabs_utils.dart'; import 'tabs_utils.dart';
Widget boilerplate({ Widget? child, TextDirection textDirection = TextDirection.ltr, bool? useMaterial3, TabBarTheme? tabBarTheme }) { Widget boilerplate({
Widget? child,
TextDirection textDirection = TextDirection.ltr,
ThemeData? theme,
TabBarTheme? tabBarTheme,
bool? useMaterial3,
}) {
return Theme( return Theme(
data: ThemeData(useMaterial3: useMaterial3, tabBarTheme: tabBarTheme), data: theme ?? ThemeData(useMaterial3: useMaterial3, tabBarTheme: tabBarTheme),
child: Localizations( child: Localizations(
locale: const Locale('en', 'US'), locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[ delegates: const <LocalizationsDelegate<dynamic>>[
...@@ -346,45 +352,48 @@ void main() { ...@@ -346,45 +352,48 @@ void main() {
}); });
testWidgets('TabBar default tab indicator (primary)', (WidgetTester tester) async { testWidgets('TabBar default tab indicator (primary)', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true); final ThemeData theme = ThemeData();
final List<Widget> tabs = List<Widget>.generate(4, (int index) { final List<Widget> tabs = List<Widget>.generate(4, (int index) {
return Tab(text: 'Tab $index'); return Tab(text: 'Tab $index');
}); });
final TabController controller = createTabController( final TabController controller = createTabController(
vsync: const TestVSync(), vsync: const TestVSync(),
length: tabs.length, length: tabs.length,
); );
const double indicatorWeightLabel = 3.0;
const double indicatorWeightTab = 2.0;
await tester.pumpWidget( Widget buildTab({ TabBarIndicatorSize? indicatorSize }) {
MaterialApp( return MaterialApp(
home: boilerplate( home: boilerplate(
useMaterial3: theme.useMaterial3, theme: theme,
child: Container( child: Container(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: TabBar( child: TabBar(
indicatorSize: indicatorSize,
controller: controller, controller: controller,
tabs: tabs, tabs: tabs,
), ),
), ),
), ),
), );
); }
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); // Test default tab indicator (TabBarIndicatorSize.label).
expect(tabBarBox.size.height, 48.0); await tester.pumpWidget(buildTab());
const double indicatorWeight = 3.0; RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(tabBarBox.size.height, 48.0);
// Check tab indicator size and color.
final RRect rrect = RRect.fromLTRBAndCorners( final RRect rrect = RRect.fromLTRBAndCorners(
64.75, 64.75,
tabBarBox.size.height - indicatorWeight, tabBarBox.size.height - indicatorWeightLabel,
135.25, 135.25,
tabBarBox.size.height, tabBarBox.size.height,
topLeft: const Radius.circular(3.0), topLeft: const Radius.circular(3.0),
topRight: const Radius.circular(3.0), topRight: const Radius.circular(3.0),
); );
expect( expect(
tabBarBox, tabBarBox,
paints paints
...@@ -392,23 +401,52 @@ void main() { ...@@ -392,23 +401,52 @@ void main() {
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
rrect: rrect, rrect: rrect,
)); ));
// Test default tab indicator (TabBarIndicatorSize.tab).
await tester.pumpWidget(buildTab(indicatorSize: TabBarIndicatorSize.tab));
await tester.pumpAndSettle();
tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(tabBarBox.size.height, 48.0);
const double indicatorY = 48 - (indicatorWeightTab / 2.0);
const double indicatorLeft = indicatorWeightTab / 2.0;
const double indicatorRight = 200.0 - (indicatorWeightTab / 2.0);
// Check tab indicator size and color.
expect(
tabBarBox,
paints
// Divider.
..line(
color: theme.colorScheme.outlineVariant,
)
// Tab indicator.
..line(
color: theme.colorScheme.primary,
strokeWidth: indicatorWeightTab,
p1: const Offset(indicatorLeft, indicatorY),
p2: const Offset(indicatorRight, indicatorY),
),
);
}); });
testWidgets('TabBar default tab indicator (secondary)', (WidgetTester tester) async { testWidgets('TabBar default tab indicator (secondary)', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true); final ThemeData theme = ThemeData();
final List<Widget> tabs = List<Widget>.generate(4, (int index) { final List<Widget> tabs = List<Widget>.generate(4, (int index) {
return Tab(text: 'Tab $index'); return Tab(text: 'Tab $index');
}); });
final TabController controller = createTabController( final TabController controller = createTabController(
vsync: const TestVSync(), vsync: const TestVSync(),
length: tabs.length, length: tabs.length,
); );
const double indicatorWeight = 2.0;
// Test default tab indicator.
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: boilerplate( home: boilerplate(
useMaterial3: theme.useMaterial3, theme: theme,
child: Container( child: Container(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: TabBar.secondary( child: TabBar.secondary(
...@@ -423,26 +461,26 @@ void main() { ...@@ -423,26 +461,26 @@ void main() {
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(tabBarBox.size.height, 48.0); expect(tabBarBox.size.height, 48.0);
const double indicatorWeight = 2.0;
const double indicatorY = 48 - (indicatorWeight / 2.0); const double indicatorY = 48 - (indicatorWeight / 2.0);
const double indicatorLeft = indicatorWeight / 2.0; const double indicatorLeft = indicatorWeight / 2.0;
const double indicatorRight = 200.0 - (indicatorWeight / 2.0); const double indicatorRight = 200.0 - (indicatorWeight / 2.0);
// Check tab indicator size and color.
expect( expect(
tabBarBox, tabBarBox,
paints paints
// Divider // Divider.
..line( ..line(
color: theme.colorScheme.outlineVariant, color: theme.colorScheme.outlineVariant,
) )
// Tab indicator // Tab indicator.
..line( ..line(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
strokeWidth: indicatorWeight, strokeWidth: indicatorWeight,
p1: const Offset(indicatorLeft, indicatorY), p1: const Offset(indicatorLeft, indicatorY),
p2: const Offset(indicatorRight, indicatorY), p2: const Offset(indicatorRight, indicatorY),
), ),
); );
}); });
testWidgets('TabBar default overlay (primary)', (WidgetTester tester) async { testWidgets('TabBar default overlay (primary)', (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