Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions packages/material_ui/lib/src/tabs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1066,8 +1066,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.tabAlignment,
this.textScaler,
this.indicatorAnimation,
}) : _isPrimary = true,
assert(indicator != null || (indicatorWeight > 0.0));
}) : _isPrimary = true;

/// Creates a Material Design secondary tab bar.
///
Expand Down Expand Up @@ -1128,8 +1127,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.tabAlignment,
this.textScaler,
this.indicatorAnimation,
}) : _isPrimary = false,
assert(indicator != null || (indicatorWeight > 0.0));
}) : _isPrimary = false;

/// Typically a list of two or more [Tab] widgets.
///
Expand Down Expand Up @@ -1185,7 +1183,8 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// If [ThemeData.useMaterial3] is false, the default value is 2.0.
///
/// If [indicator] is specified or provided from [TabBarThemeData],
/// this property is ignored.
/// this property is ignored. Otherwise, when drawing the default underline
/// indicator, the value of this parameter must be greater than zero.
final double indicatorWeight;

/// The padding for the indicator.
Expand Down Expand Up @@ -1644,6 +1643,24 @@ class _TabBarState extends State<TabBar> {
if (tabBarTheme.indicator != null) {
return tabBarTheme.indicator!;
}
assert(() {
if (widget.indicatorWeight <= 0.0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using !(widget.indicatorWeight > 0.0) instead of widget.indicatorWeight <= 0.0 is more robust because it also correctly handles double.nan values, ensuring that any invalid weight triggers the assertion rather than potentially causing silent rendering issues or secondary errors later.

      if (!(widget.indicatorWeight > 0.0)) {

throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Invalid indicatorWeight for TabBar.'),
ErrorDescription(
'The indicatorWeight must be greater than zero when the TabBar '
'draws its default underline indicator, that is when no indicator '
'is provided by the TabBar or the TabBarTheme.',
),
Comment on lines +1650 to +1654

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with the rest of the error message (such as the ErrorHint below and the documentation), refer to TabBarThemeData instead of TabBarTheme in the error description.

          ErrorDescription(
            'The indicatorWeight must be greater than zero when the TabBar '
            'draws its default underline indicator, that is when no indicator '
            'is provided by the TabBar or the TabBarThemeData.',
          ),

ErrorHint(
'To fix this, set indicatorWeight to a value greater than zero, '
'or provide an indicator with the TabBar.indicator or '
'TabBarThemeData.indicator property.',
),
]);
}
return true;
}());

Color color = widget.indicatorColor ?? tabBarTheme.indicatorColor ?? _defaults.indicatorColor!;
// ThemeData tries to avoid this by having indicatorColor avoid being the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changelog: |
- Allows `TabBar.indicatorWeight` of 0 when the indicator is supplied by `TabBarThemeData`.
version: patch
100 changes: 100 additions & 0 deletions packages/material_ui/test/tabs_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
import 'package:material_ui/material_ui.dart';

import 'feedback_tester.dart';
Expand Down Expand Up @@ -3646,6 +3647,105 @@ void main() {
);
});

testWidgets('TabBar accepts indicatorWeight: 0 when the indicator comes from TabBarThemeData', (
WidgetTester tester,
) async {
// Regression test for https://github.com/flutter/flutter/issues/188837.
// A themed indicator supplies the decoration, so indicatorWeight: 0 must not
// throw the default-underline assertion.
const indicatorColor = Color(0xFF00FF00);
const Decoration indicator = BoxDecoration(color: indicatorColor);
const tabs = <Widget>[Tab(text: 'A'), Tab(text: 'B')];

Widget buildFrame({required bool secondary}) {
return boilerplate(
tabBarTheme: const TabBarThemeData(
indicator: indicator,
indicatorSize: TabBarIndicatorSize.tab,
),
child: Container(
alignment: Alignment.topLeft,
child: DefaultTabController(
length: tabs.length,
child: secondary
? const TabBar.secondary(indicatorWeight: 0.0, tabs: tabs)
: const TabBar(indicatorWeight: 0.0, tabs: tabs),
),
),
);
}

// Primary TabBar: no assertion is thrown and the themed indicator is painted.
await tester.pumpWidget(buildFrame(secondary: false));
expect(tester.takeException(), isNull);

final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
// 46 = _kTabHeight(46) with no weight added by the zero indicatorWeight.
expect(tabBarBox.size.height, 46.0);
expect(
tabBarBox,
paints..rect(rect: const Rect.fromLTRB(0.0, 0.0, 400.0, 46.0), color: indicatorColor),
);

// Secondary TabBar: also accepts a zero indicatorWeight with a themed indicator.
await tester.pumpWidget(buildFrame(secondary: true));
expect(tester.takeException(), isNull);
});

testWidgets('TabBar accepts indicatorWeight: 0 when the indicator is provided on the widget', (
WidgetTester tester,
) async {
const Decoration indicator = BoxDecoration(color: Color(0xFF00FF00));
const tabs = <Widget>[Tab(text: 'A'), Tab(text: 'B')];

await tester.pumpWidget(
boilerplate(
child: DefaultTabController(
length: tabs.length,
child: const TabBar(indicator: indicator, indicatorWeight: 0.0, tabs: tabs),
),
),
);

expect(tester.takeException(), isNull);
});

testWidgets(
'TabBar throws indicatorWeight: 0 with the default underline indicator',
experimentalLeakTesting: LeakTesting.settings
.withIgnoredAll(), // leaking by design because of exception
(WidgetTester tester) async {
// With no indicator on the widget or the theme, the TabBar falls back to the
// default underline indicator, which requires a positive indicatorWeight.
const tabs = <Widget>[Tab(text: 'A'), Tab(text: 'B')];
final Matcher throwsInvalidIndicatorWeightError = isFlutterError.having(
(FlutterError error) => error.message,
'message',
contains('Invalid indicatorWeight for TabBar.'),
);

await tester.pumpWidget(
boilerplate(
child: DefaultTabController(
length: tabs.length,
child: const TabBar(indicatorWeight: 0.0, tabs: tabs),
),
),
);
expect(tester.takeException(), throwsInvalidIndicatorWeightError);

await tester.pumpWidget(
boilerplate(
child: DefaultTabController(
length: tabs.length,
child: const TabBar.secondary(indicatorWeight: 0.0, tabs: tabs),
),
),
);
expect(tester.takeException(), throwsInvalidIndicatorWeightError);
},
);

testWidgets('TabBar with custom indicator - directional indicatorPadding (LTR)', (
WidgetTester tester,
) async {
Expand Down