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