Unverified Commit a732a748 authored by Taha Tesser's avatar Taha Tesser Committed by GitHub

Introduce `TabBar.tabAlignment` (#125036)

fixes https://github.com/flutter/flutter/issues/124195

This introduces `TabBar.tabAlignment` while keeping the default alignment for both M2 and M3.
parent e2ddf563
......@@ -13,12 +13,13 @@ class TabsTemplate extends TokenTemplate {
@override
String generate() => '''
class _${blockName}PrimaryDefaultsM3 extends TabBarTheme {
_${blockName}PrimaryDefaultsM3(this.context)
_${blockName}PrimaryDefaultsM3(this.context, this.isScrollable)
: super(indicatorSize: TabBarIndicatorSize.label);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
late final TextTheme _textTheme = Theme.of(context).textTheme;
final bool isScrollable;
@override
Color? get dividerColor => ${componentColor("md.comp.primary-navigation-tab.divider")};
......@@ -68,15 +69,19 @@ class _${blockName}PrimaryDefaultsM3 extends TabBarTheme {
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
}
class _${blockName}SecondaryDefaultsM3 extends TabBarTheme {
_${blockName}SecondaryDefaultsM3(this.context)
_${blockName}SecondaryDefaultsM3(this.context, this.isScrollable)
: super(indicatorSize: TabBarIndicatorSize.tab);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
late final TextTheme _textTheme = Theme.of(context).textTheme;
final bool isScrollable;
@override
Color? get dividerColor => ${componentColor("md.comp.secondary-navigation-tab.divider")};
......@@ -126,6 +131,9 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarTheme {
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
}
''';
......
......@@ -40,6 +40,7 @@ class TabBarTheme with Diagnosticable {
this.overlayColor,
this.splashFactory,
this.mouseCursor,
this.tabAlignment,
});
/// Overrides the default value for [TabBar.indicator].
......@@ -90,6 +91,9 @@ class TabBarTheme with Diagnosticable {
/// If specified, overrides the default value of [TabBar.mouseCursor].
final MaterialStateProperty<MouseCursor?>? mouseCursor;
/// Overrides the default value for [TabBar.tabAlignment].
final TabAlignment? tabAlignment;
/// Creates a copy of this object but with the given fields replaced with the
/// new values.
TabBarTheme copyWith({
......@@ -105,6 +109,7 @@ class TabBarTheme with Diagnosticable {
MaterialStateProperty<Color?>? overlayColor,
InteractiveInkFeatureFactory? splashFactory,
MaterialStateProperty<MouseCursor?>? mouseCursor,
TabAlignment? tabAlignment,
}) {
return TabBarTheme(
indicator: indicator ?? this.indicator,
......@@ -119,6 +124,7 @@ class TabBarTheme with Diagnosticable {
overlayColor: overlayColor ?? this.overlayColor,
splashFactory: splashFactory ?? this.splashFactory,
mouseCursor: mouseCursor ?? this.mouseCursor,
tabAlignment: tabAlignment ?? this.tabAlignment,
);
}
......@@ -149,6 +155,7 @@ class TabBarTheme with Diagnosticable {
overlayColor: MaterialStateProperty.lerp<Color?>(a.overlayColor, b.overlayColor, t, Color.lerp),
splashFactory: t < 0.5 ? a.splashFactory : b.splashFactory,
mouseCursor: t < 0.5 ? a.mouseCursor : b.mouseCursor,
tabAlignment: t < 0.5 ? a.tabAlignment : b.tabAlignment,
);
}
......@@ -166,6 +173,7 @@ class TabBarTheme with Diagnosticable {
overlayColor,
splashFactory,
mouseCursor,
tabAlignment,
);
@override
......@@ -188,6 +196,7 @@ class TabBarTheme with Diagnosticable {
&& other.unselectedLabelStyle == unselectedLabelStyle
&& other.overlayColor == overlayColor
&& other.splashFactory == splashFactory
&& other.mouseCursor == mouseCursor;
&& other.mouseCursor == mouseCursor
&& other.tabAlignment == tabAlignment;
}
}
......@@ -49,6 +49,41 @@ enum TabBarIndicatorSize {
label,
}
/// Defines how tabs are aligned horizontally in a [TabBar].
///
/// See also:
///
/// * [TabBar], which displays a row of tabs.
/// * [TabBarView], which displays a widget for the currently selected tab.
/// * [TabBar.tabAlignment], which defines the horizontal alignment of the
/// tabs within the [TabBar].
enum TabAlignment {
// TODO(tahatesser): Add a link to the Material Design spec for
// horizontal offset when it is available.
// It's currently sourced from androidx/compose/material3/TabRow.kt.
/// If [TabBar.isScrollable] is true, tabs are aligned to the
/// start of the [TabBar]. Otherwise throws an exception.
///
/// It is not recommended to set [TabAlignment.start] when
/// [ThemeData.useMaterial3] is false.
start,
/// If [TabBar.isScrollable] is true, tabs are aligned to the
/// start of the [TabBar] with an offset of 52.0 pixels.
/// Otherwise throws an exception.
///
/// It is not recommended to set [TabAlignment.startOffset] when
/// [ThemeData.useMaterial3] is false.
startOffset,
/// If [TabBar.isScrollable] is false, tabs are stretched to fill the
/// [TabBar]. Otherwise throws an exception.
fill,
/// Tabs are aligned to the center of the [TabBar].
center,
}
/// A Material Design [TabBar] tab.
///
/// If both [icon] and [text] are provided, the text is displayed below
......@@ -306,9 +341,9 @@ class _TabLabelBar extends Flex {
const _TabLabelBar({
super.children,
required this.onPerformLayout,
required super.mainAxisSize,
}) : super(
direction: Axis.horizontal,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
verticalDirection: VerticalDirection.down,
......@@ -695,6 +730,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.physics,
this.splashFactory,
this.splashBorderRadius,
this.tabAlignment,
}) : _isPrimary = true,
assert(indicator != null || (indicatorWeight > 0.0));
......@@ -744,6 +780,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.physics,
this.splashFactory,
this.splashBorderRadius,
this.tabAlignment,
}) : _isPrimary = false,
assert(indicator != null || (indicatorWeight > 0.0));
......@@ -1027,6 +1064,25 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// If this property is null, it is interpreted as [BorderRadius.zero].
final BorderRadius? splashBorderRadius;
/// Specifies the horizontal alignment of the tabs within a [TabBar].
///
/// If [TabBar.isScrollable] is false, only [TabAlignment.fill] and
/// [TabAlignment.center] are supported. Otherwise an exception is thrown.
///
/// If [TabBar.isScrollable] is true, only [TabAlignment.start], [TabAlignment.startOffset],
/// and [TabAlignment.center] are supported. Otherwise an exception is thrown.
///
/// If this is null, then the value of [TabBarTheme.tabAlignment] is used.
///
/// If [TabBarTheme.tabAlignment] is null and [ThemeData.useMaterial3] is true,
/// then [TabAlignment.startOffset] is used if [isScrollable] is true,
/// otherwise [TabAlignment.fill] is used.
///
/// If [TabBarTheme.tabAlignment] is null and [ThemeData.useMaterial3] is false,
/// then [TabAlignment.center] is used if [isScrollable] is true,
/// otherwise [TabAlignment.fill] is used.
final TabAlignment? tabAlignment;
/// A size whose height depends on if the tabs have both icons and text.
///
/// [AppBar] uses this size to compute its own preferred size.
......@@ -1089,10 +1145,10 @@ class _TabBarState extends State<TabBar> {
TabBarTheme get _defaults {
if (Theme.of(context).useMaterial3) {
return widget._isPrimary
? _TabsPrimaryDefaultsM3(context)
: _TabsSecondaryDefaultsM3(context);
? _TabsPrimaryDefaultsM3(context, widget.isScrollable)
: _TabsSecondaryDefaultsM3(context, widget.isScrollable);
} else {
return _TabsDefaultsM2(context);
return _TabsDefaultsM2(context, widget.isScrollable);
}
}
......@@ -1378,10 +1434,32 @@ class _TabBarState extends State<TabBar> {
return true;
}
bool _debugTabAlignmentIsValid(TabAlignment tabAlignment) {
assert(() {
if (widget.isScrollable && tabAlignment == TabAlignment.fill) {
throw FlutterError(
'$tabAlignment is only valid for non-scrollable tab bars.',
);
}
if (!widget.isScrollable
&& (tabAlignment == TabAlignment.start
|| tabAlignment == TabAlignment.startOffset)) {
throw FlutterError(
'$tabAlignment is only valid for scrollable tab bars.',
);
}
return true;
}());
return true;
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
assert(_debugScheduleCheckHasValidTabsCount());
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final TabAlignment effectiveTabAlignment = widget.tabAlignment ?? tabBarTheme.tabAlignment ?? _defaults.tabAlignment!;
assert(_debugTabAlignmentIsValid(effectiveTabAlignment));
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
if (_controller!.length == 0) {
......@@ -1390,7 +1468,6 @@ class _TabBarState extends State<TabBar> {
);
}
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final List<Widget> wrappedTabs = List<Widget>.generate(widget.tabs.length, (int index) {
const double verticalAdjustment = (_kTextAndIconTabHeight - _kTabHeight)/2.0;
......@@ -1491,7 +1568,7 @@ class _TabBarState extends State<TabBar> {
),
),
);
if (!widget.isScrollable) {
if (!widget.isScrollable && effectiveTabAlignment == TabAlignment.fill) {
wrappedTabs[index] = Expanded(child: wrappedTabs[index]);
}
}
......@@ -1509,12 +1586,16 @@ class _TabBarState extends State<TabBar> {
defaults: _defaults,
child: _TabLabelBar(
onPerformLayout: _saveTabOffsets,
mainAxisSize: effectiveTabAlignment == TabAlignment.fill ? MainAxisSize.max : MainAxisSize.min,
children: wrappedTabs,
),
),
);
if (widget.isScrollable) {
final EdgeInsetsGeometry? effectivePadding = effectiveTabAlignment == TabAlignment.startOffset
? const EdgeInsetsDirectional.only(start: 56.0).add(widget.padding ?? EdgeInsets.zero)
: widget.padding;
_scrollController ??= _TabBarScrollController(this);
tabBar = ScrollConfiguration(
// The scrolling tabs should not show an overscroll indicator.
......@@ -1523,7 +1604,7 @@ class _TabBarState extends State<TabBar> {
dragStartBehavior: widget.dragStartBehavior,
scrollDirection: Axis.horizontal,
controller: _scrollController,
padding: widget.padding,
padding: effectivePadding,
physics: widget.physics,
child: tabBar,
),
......@@ -2030,10 +2111,11 @@ class TabPageSelector extends StatelessWidget {
// Hand coded defaults based on Material Design 2.
class _TabsDefaultsM2 extends TabBarTheme {
const _TabsDefaultsM2(this.context)
const _TabsDefaultsM2(this.context, this.isScrollable)
: super(indicatorSize: TabBarIndicatorSize.tab);
final BuildContext context;
final bool isScrollable;
@override
Color? get indicatorColor => Theme.of(context).indicatorColor;
......@@ -2049,6 +2131,9 @@ class _TabsDefaultsM2 extends TabBarTheme {
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
}
// BEGIN GENERATED TOKEN PROPERTIES - Tabs
......@@ -2061,12 +2146,13 @@ class _TabsDefaultsM2 extends TabBarTheme {
// Token database version: v0_162
class _TabsPrimaryDefaultsM3 extends TabBarTheme {
_TabsPrimaryDefaultsM3(this.context)
_TabsPrimaryDefaultsM3(this.context, this.isScrollable)
: super(indicatorSize: TabBarIndicatorSize.label);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
late final TextTheme _textTheme = Theme.of(context).textTheme;
final bool isScrollable;
@override
Color? get dividerColor => _colors.surfaceVariant;
......@@ -2116,15 +2202,19 @@ class _TabsPrimaryDefaultsM3 extends TabBarTheme {
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
}
class _TabsSecondaryDefaultsM3 extends TabBarTheme {
_TabsSecondaryDefaultsM3(this.context)
_TabsSecondaryDefaultsM3(this.context, this.isScrollable)
: super(indicatorSize: TabBarIndicatorSize.tab);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
late final TextTheme _textTheme = Theme.of(context).textTheme;
final bool isScrollable;
@override
Color? get dividerColor => _colors.surfaceVariant;
......@@ -2174,6 +2264,9 @@ class _TabsSecondaryDefaultsM3 extends TabBarTheme {
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.start : TabAlignment.fill;
}
// END GENERATED TOKEN PROPERTIES - Tabs
......@@ -96,6 +96,7 @@ void main() {
expect(const TabBarTheme().overlayColor, null);
expect(const TabBarTheme().splashFactory, null);
expect(const TabBarTheme().mouseCursor, null);
expect(const TabBarTheme().tabAlignment, null);
});
test('TabBarTheme lerp special cases', () {
......@@ -138,7 +139,7 @@ void main() {
// 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.
// Test default indicator color and divider color.
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(
tabBarBox,
......@@ -189,7 +190,7 @@ void main() {
// 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.
// Test default indicator color and divider color.
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(
tabBarBox,
......@@ -464,14 +465,97 @@ void main() {
);
});
testWidgets('TabAlignment.fill from TabBarTheme only supports non-scrollable tab bar', (WidgetTester tester) async {
const TabBarTheme tabBarTheme = TabBarTheme(tabAlignment: TabAlignment.fill);
// Test TabAlignment.fill from TabBarTheme with non-scrollable tab bar.
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
expect(tester.takeException(), isNull);
// Test TabAlignment.fill from TabBarTheme with scrollable tab bar.
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme, isScrollable: true));
expect(tester.takeException(), isAssertionError);
});
testWidgets(
'TabAlignment.start & TabAlignment.startOffset from TabBarTheme only supports scrollable tab bar',
(WidgetTester tester) async {
TabBarTheme tabBarTheme = const TabBarTheme(tabAlignment: TabAlignment.start);
// Test TabAlignment.start from TabBarTheme with scrollable tab bar.
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme, isScrollable: true));
expect(tester.takeException(), isNull);
// Test TabAlignment.start from TabBarTheme with non-scrollable tab bar.
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
expect(tester.takeException(), isAssertionError);
tabBarTheme = const TabBarTheme(tabAlignment: TabAlignment.startOffset);
// Test TabAlignment.startOffset from TabBarTheme with scrollable tab bar.
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme, isScrollable: true));
expect(tester.takeException(), isNull);
// Test TabAlignment.startOffset from TabBarTheme with non-scrollable tab bar.
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
expect(tester.takeException(), isAssertionError);
});
group('Material 2', () {
// Tests that are only relevant for Material 2. Once ThemeData.useMaterial3
// is turned on by default, these tests can be removed.
testWidgets('Tab bar defaults', (WidgetTester tester) async {
// tests for the default label color and label styles when tabBarTheme and tabBar do not provide any
testWidgets('Tab bar defaults (primary)', (WidgetTester tester) async {
// Test default label color and label styles.
await tester.pumpWidget(buildTabBar());
final ThemeData theme = ThemeData(useMaterial3: false);
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
expect(selectedLabel.text.style!.fontFamily, equals('Roboto'));
expect(selectedLabel.text.style!.fontSize, equals(14.0));
expect(selectedLabel.text.style!.color, equals(Colors.white));
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
expect(unselectedLabel.text.style!.fontFamily, equals('Roboto'));
expect(unselectedLabel.text.style!.fontSize, equals(14.0));
expect(unselectedLabel.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
// Test default labelPadding.
await tester.pumpWidget(buildTabBar(tabs: _sizedTabs, isScrollable: true));
const double indicatorWeight = 2.0;
final Rect tabBar = tester.getRect(find.byType(TabBar));
final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!));
final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].key!));
// Verify 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));
// Test default indicator color.
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(tabBarBox, paints..line(color: theme.indicatorColor));
});
testWidgets('Tab bar defaults (secondary)', (WidgetTester tester) async {
// Test default label color and label styles.
await tester.pumpWidget(buildTabBar(secondaryTabBar: true));
final ThemeData theme = ThemeData(useMaterial3: false);
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
expect(selectedLabel.text.style!.fontFamily, equals('Roboto'));
expect(selectedLabel.text.style!.fontSize, equals(14.0));
......@@ -481,7 +565,7 @@ void main() {
expect(unselectedLabel.text.style!.fontSize, equals(14.0));
expect(unselectedLabel.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
// tests for the default value of labelPadding when tabBarTheme and tabBar do not provide one
// Test default labelPadding.
await tester.pumpWidget(buildTabBar(tabs: _sizedTabs, isScrollable: true));
const double indicatorWeight = 2.0;
......@@ -489,21 +573,22 @@ void main() {
final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!));
final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].key!));
// verify coordinates of tabOne
// Verify tabOne coordinates.
expect(tabOneRect.left, equals(kTabLabelPadding.left));
expect(tabOneRect.top, equals(kTabLabelPadding.top));
expect(tabOneRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight));
// verify coordinates of tabTwo
// Verify tabTwo coordinates.
expect(tabTwoRect.right, equals(tabBar.width - kTabLabelPadding.right));
expect(tabTwoRect.top, equals(kTabLabelPadding.top));
expect(tabTwoRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight));
// verify tabOne and tabTwo is separated by right padding of tabOne and left padding of tabTwo
// Verify tabOne and tabTwo is separated by right padding of tabOne and left padding of tabTwo.
expect(tabOneRect.right, equals(tabTwoRect.left - kTabLabelPadding.left - kTabLabelPadding.right));
// Test default indicator color.
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
expect(tabBarBox, paints..line(color: const Color(0xff2196f3)));
expect(tabBarBox, paints..line(color: theme.indicatorColor));
});
testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async {
......
......@@ -114,6 +114,7 @@ Widget buildFrame({
Duration? animationDuration,
EdgeInsetsGeometry? padding,
TextDirection textDirection = TextDirection.ltr,
TabAlignment? tabAlignment,
}) {
if (secondaryTabBar) {
return boilerplate(
......@@ -128,6 +129,7 @@ Widget buildFrame({
isScrollable: isScrollable,
indicatorColor: indicatorColor,
padding: padding,
tabAlignment: tabAlignment,
),
),
);
......@@ -145,6 +147,7 @@ Widget buildFrame({
isScrollable: isScrollable,
indicatorColor: indicatorColor,
padding: padding,
tabAlignment: tabAlignment,
),
),
);
......@@ -5717,12 +5720,106 @@ void main() {
);
});
testWidgets('Default TabAlignment', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
final List<String> tabs = <String>['A', 'B'];
// Test default TabAlignment when isScrollable is false.
await tester.pumpWidget(MaterialApp(
theme: theme,
home: buildFrame(tabs: tabs, value: 'B'),
));
final Rect tabBar = tester.getRect(find.byType(TabBar));
Rect tabOneRect = tester.getRect(find.byType(Tab).first);
Rect tabTwoRect = tester.getRect(find.byType(Tab).last);
// Tabs should fill the width of the TabBar.
double tabOneLeft = ((tabBar.width / 2) - tabOneRect.width) / 2;
expect(tabOneRect.left, equals(tabOneLeft));
double tabTwoRight = tabBar.width - ((tabBar.width / 2) - tabTwoRect.width) / 2;
expect(tabTwoRect.right, equals(tabTwoRight));
// Test default TabAlignment when isScrollable is true.
await tester.pumpWidget(MaterialApp(
theme: theme,
home: buildFrame(tabs: tabs, value: 'B', isScrollable: true),
));
tabOneRect = tester.getRect(find.byType(Tab).first);
tabTwoRect = tester.getRect(find.byType(Tab).last);
// Tabs should be aligned to the start of the TabBar.
tabOneLeft = kTabLabelPadding.left;
expect(tabOneRect.left, equals(tabOneLeft));
tabTwoRight = kTabLabelPadding.horizontal + tabOneRect.width + kTabLabelPadding.left + tabTwoRect.width;
expect(tabTwoRect.right, equals(tabTwoRight));
});
testWidgets('TabAlignment.fill only supports non-scrollable tab bar', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
final List<String> tabs = <String>['A', 'B'];
// Test TabAlignment.fill with non-scrollable tab bar.
await tester.pumpWidget(MaterialApp(
theme: theme,
home: buildFrame(tabs: tabs, value: 'B', tabAlignment: TabAlignment.fill),
));
expect(tester.takeException(), isNull);
// Test TabAlignment.fill with scrollable tab bar.
await tester.pumpWidget(MaterialApp(
theme: theme,
home: buildFrame(tabs: tabs, value: 'B', tabAlignment: TabAlignment.fill, isScrollable: true),
));
expect(tester.takeException(), isAssertionError);
});
testWidgets('TabAlignment.start & TabAlignment.startOffset only supports scrollable tab bar', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
final List<String> tabs = <String>['A', 'B'];
// Test TabAlignment.start with scrollable tab bar.
await tester.pumpWidget(MaterialApp(
theme: theme,
home: buildFrame(tabs: tabs, value: 'B', tabAlignment: TabAlignment.start, isScrollable: true),
));
expect(tester.takeException(), isNull);
// Test TabAlignment.start with non-scrollable tab bar.
await tester.pumpWidget(MaterialApp(
theme: theme,
home: buildFrame(tabs: tabs, value: 'B', tabAlignment: TabAlignment.start),
));
expect(tester.takeException(), isAssertionError);
// Test TabAlignment.startOffset with scrollable tab bar.
await tester.pumpWidget(MaterialApp(
theme: theme,
home: buildFrame(tabs: tabs, value: 'B', tabAlignment: TabAlignment.startOffset, isScrollable: true),
));
expect(tester.takeException(), isNull);
// Test TabAlignment.startOffset with non-scrollable tab bar.
await tester.pumpWidget(MaterialApp(
theme: theme,
home: buildFrame(tabs: tabs, value: 'B', tabAlignment: TabAlignment.startOffset),
));
expect(tester.takeException(), isAssertionError);
});
group('Material 2', () {
// Tests that are only relevant for Material 2. Once ThemeData.useMaterial3
// is turned on by default, these tests can be removed.
testWidgets('TabBar default selected/unselected text style', (WidgetTester tester) async {
final ThemeData theme = ThemeData();
final ThemeData theme = ThemeData(useMaterial3: false);
final List<String> tabs = <String>['A', 'B', 'C'];
const String selectedValue = 'A';
......@@ -5758,7 +5855,10 @@ void main() {
const Color labelColor = Color(0xff0000ff);
await tester.pumpWidget(
Theme(
data: ThemeData(tabBarTheme: const TabBarTheme(labelColor: labelColor)),
data: ThemeData(
tabBarTheme: const TabBarTheme(labelColor: labelColor),
useMaterial3: false,
),
child: buildFrame(tabs: tabs, value: selectedValue),
),
);
......@@ -5808,6 +5908,42 @@ void main() {
// ignore: avoid_dynamic_calls
expect((paint.painter as dynamic).dividerColor, dividerColor);
});
testWidgets('Default TabAlignment', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: false);
final List<String> tabs = <String>['A', 'B'];
// Test default TabAlignment when isScrollable is false.
await tester.pumpWidget(MaterialApp(
theme: theme,
home: buildFrame(tabs: tabs, value: 'B'),
));
final Rect tabBar = tester.getRect(find.byType(TabBar));
Rect tabOneRect = tester.getRect(find.byType(Tab).first);
Rect tabTwoRect = tester.getRect(find.byType(Tab).last);
// Tabs should fill the width of the TabBar.
double tabOneLeft = ((tabBar.width / 2) - tabOneRect.width) / 2;
expect(tabOneRect.left, equals(tabOneLeft));
double tabTwoRight = tabBar.width - ((tabBar.width / 2) - tabTwoRect.width) / 2;
expect(tabTwoRect.right, equals(tabTwoRight));
// Test default TabAlignment when isScrollable is true.
await tester.pumpWidget(MaterialApp(
theme: theme,
home: buildFrame(tabs: tabs, value: 'B', isScrollable: true),
));
tabOneRect = tester.getRect(find.byType(Tab).first);
tabTwoRect = tester.getRect(find.byType(Tab).last);
// Tabs should be aligned to the start of the TabBar.
tabOneLeft = kTabLabelPadding.left;
expect(tabOneRect.left, equals(tabOneLeft));
tabTwoRight = kTabLabelPadding.horizontal + tabOneRect.width + kTabLabelPadding.left + tabTwoRect.width;
expect(tabTwoRect.right, equals(tabTwoRight));
});
});
}
......
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