flutter/dev/tools/gen_defaults/lib/tabs_template.dart
Taha Tesser 2c71881f50
Fix Scrollable TabBar for Material 3 (#131409)
fixes [Material 3 `TabBar` does not take full width when `isScrollable: true`](https://github.com/flutter/flutter/issues/117722)

### Description
1. Fixed the divider doesn't stretch to take all the available width in the scrollable tab bar in M3
2. Added `dividerHeight` property.

### Code sample

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

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

/// Flutter code sample for [TabBar].

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: TabBarExample(),
    );
  }
}

class TabBarExample extends StatefulWidget {
  const TabBarExample({super.key});

  @override
  State<TabBarExample> createState() => _TabBarExampleState();
}

class _TabBarExampleState extends State<TabBarExample> {
  bool rtl = false;
  bool customColors = false;
  bool removeDivider = false;
  Color dividerColor = Colors.amber;
  Color indicatorColor = Colors.red;

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      initialIndex: 1,
      length: 3,
      child: Directionality(
        textDirection: rtl ? TextDirection.rtl : TextDirection.ltr,
        child: Scaffold(
          appBar: AppBar(
            title: const Text('TabBar Sample'),
            actions: <Widget>[
              IconButton.filledTonal(
                tooltip: 'Switch direction',
                icon: const Icon(Icons.swap_horiz),
                onPressed: () {
                  setState(() {
                    rtl = !rtl;
                  });
                },
              ),
              IconButton.filledTonal(
                tooltip: 'Use custom colors',
                icon: const Icon(Icons.color_lens),
                onPressed: () {
                  setState(() {
                    customColors = !customColors;
                  });
                },
              ),
              IconButton.filledTonal(
                tooltip: 'Show/hide divider',
                icon: const Icon(Icons.remove_rounded),
                onPressed: () {
                  setState(() {
                    removeDivider = !removeDivider;
                  });
                },
              ),
            ],
          ),
          body: Column(
            children: <Widget>[
              const Spacer(),
              const Text('Scrollable - TabAlignment.start'),
              TabBar(
                isScrollable: true,
                tabAlignment: TabAlignment.start,
                dividerColor: customColors ? dividerColor : null,
                indicatorColor: customColors ? indicatorColor : null,
                dividerHeight: removeDivider ? 0 : null,
                tabs: const <Widget>[
                  Tab(
                    icon: Icon(Icons.cloud_outlined),
                  ),
                  Tab(
                    icon: Icon(Icons.beach_access_sharp),
                  ),
                  Tab(
                    icon: Icon(Icons.brightness_5_sharp),
                  ),
                ],
              ),
              const Text('Scrollable - TabAlignment.startOffset'),
              TabBar(
                isScrollable: true,
                tabAlignment: TabAlignment.startOffset,
                dividerColor: customColors ? dividerColor : null,
                indicatorColor: customColors ? indicatorColor : null,
                dividerHeight: removeDivider ? 0 : null,
                tabs: const <Widget>[
                  Tab(
                    icon: Icon(Icons.cloud_outlined),
                  ),
                  Tab(
                    icon: Icon(Icons.beach_access_sharp),
                  ),
                  Tab(
                    icon: Icon(Icons.brightness_5_sharp),
                  ),
                ],
              ),
              const Text('Scrollable - TabAlignment.center'),
              TabBar(
                isScrollable: true,
                tabAlignment: TabAlignment.center,
                dividerColor: customColors ? dividerColor : null,
                indicatorColor: customColors ? indicatorColor : null,
                dividerHeight: removeDivider ? 0 : null,
                tabs: const <Widget>[
                  Tab(
                    icon: Icon(Icons.cloud_outlined),
                  ),
                  Tab(
                    icon: Icon(Icons.beach_access_sharp),
                  ),
                  Tab(
                    icon: Icon(Icons.brightness_5_sharp),
                  ),
                ],
              ),
              const Spacer(),
              const Text('Non-scrollable - TabAlignment.fill'),
              TabBar(
                tabAlignment: TabAlignment.fill,
                dividerColor: customColors ? dividerColor : null,
                indicatorColor: customColors ? indicatorColor : null,
                dividerHeight: removeDivider ? 0 : null,
                tabs: const <Widget>[
                  Tab(
                    icon: Icon(Icons.cloud_outlined),
                  ),
                  Tab(
                    icon: Icon(Icons.beach_access_sharp),
                  ),
                  Tab(
                    icon: Icon(Icons.brightness_5_sharp),
                  ),
                ],
              ),
              const Text('Non-scrollable - TabAlignment.center'),
              TabBar(
                tabAlignment: TabAlignment.center,
                dividerColor: customColors ? dividerColor : null,
                indicatorColor: customColors ? indicatorColor : null,
                dividerHeight: removeDivider ? 0 : null,
                tabs: const <Widget>[
                  Tab(
                    icon: Icon(Icons.cloud_outlined),
                  ),
                  Tab(
                    icon: Icon(Icons.beach_access_sharp),
                  ),
                  Tab(
                    icon: Icon(Icons.brightness_5_sharp),
                  ),
                ],
              ),
              const Spacer(),
              const Text('Secondary - TabAlignment.fill'),
              TabBar.secondary(
                tabAlignment: TabAlignment.fill,
                dividerColor: customColors ? dividerColor : null,
                indicatorColor: customColors ? indicatorColor : null,
                dividerHeight: removeDivider ? 0 : null,
                tabs: const <Widget>[
                  Tab(
                    icon: Icon(Icons.cloud_outlined),
                  ),
                  Tab(
                    icon: Icon(Icons.beach_access_sharp),
                  ),
                  Tab(
                    icon: Icon(Icons.brightness_5_sharp),
                  ),
                ],
              ),
              const Text('Secondary - TabAlignment.center'),
              TabBar.secondary(
                tabAlignment: TabAlignment.center,
                dividerColor: customColors ? dividerColor : null,
                indicatorColor: customColors ? indicatorColor : null,
                dividerHeight: removeDivider ? 0 : null,
                tabs: const <Widget>[
                  Tab(
                    icon: Icon(Icons.cloud_outlined),
                  ),
                  Tab(
                    icon: Icon(Icons.beach_access_sharp),
                  ),
                  Tab(
                    icon: Icon(Icons.brightness_5_sharp),
                  ),
                ],
              ),
              const Spacer(),
            ],
          ),
        ),
      ),
    );
  }
}
``` 
	
</details>

### Before

![Screenshot 2023-07-27 at 14 12 36](https://github.com/flutter/flutter/assets/48603081/1c08a9d2-ac15-4d33-8fa1-c765b4b10f92)

### After 

![Screenshot 2023-07-27 at 14 13 12](https://github.com/flutter/flutter/assets/48603081/7e662dfe-9f32-46c9-a128-3024a4782882)

This also contains regression test for https://github.com/flutter/flutter/pull/125974#discussion_r1239089151

```dart
  // This is a regression test for https://github.com/flutter/flutter/pull/125974#discussion_r1239089151.
  testWidgets('Divider can be constrained', (WidgetTester tester) async {
```

![Screenshot 2023-07-27 at 14 16 37](https://github.com/flutter/flutter/assets/48603081/ac2ef49b-2410-46d0-8ae2-d9b77236abba)
2023-08-02 00:48:06 +00:00

149 lines
5.6 KiB
Dart

// 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 'template.dart';
class TabsTemplate extends TokenTemplate {
const TabsTemplate(super.blockName, super.fileName, super.tokens, {
super.colorSchemePrefix = '_colors.',
super.textThemePrefix = '_textTheme.',
});
@override
String generate() => '''
class _${blockName}PrimaryDefaultsM3 extends TabBarTheme {
_${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")};
@override
double? get dividerHeight => ${getToken('md.comp.primary-navigation-tab.divider.height')};
@override
Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")};
@override
Color? get labelColor => ${componentColor("md.comp.primary-navigation-tab.with-label-text.active.label-text")};
@override
TextStyle? get labelStyle => ${textStyle("md.comp.primary-navigation-tab.with-label-text.label-text")};
@override
Color? get unselectedLabelColor => ${componentColor("md.comp.primary-navigation-tab.with-label-text.inactive.label-text")};
@override
TextStyle? get unselectedLabelStyle => ${textStyle("md.comp.primary-navigation-tab.with-label-text.label-text")};
@override
MaterialStateProperty<Color?> get overlayColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.primary-navigation-tab.active.pressed.state-layer')};
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.primary-navigation-tab.active.hover.state-layer')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.primary-navigation-tab.active.focus.state-layer')};
}
return null;
}
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.primary-navigation-tab.inactive.pressed.state-layer')};
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.primary-navigation-tab.inactive.hover.state-layer')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.primary-navigation-tab.inactive.focus.state-layer')};
}
return null;
});
}
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
static double indicatorWeight = ${getToken('md.comp.primary-navigation-tab.active-indicator.height')};
}
class _${blockName}SecondaryDefaultsM3 extends TabBarTheme {
_${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")};
@override
double? get dividerHeight => ${getToken('md.comp.secondary-navigation-tab.divider.height')};
@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.pressed)) {
return ${componentColor('md.comp.secondary-navigation-tab.pressed.state-layer')};
}
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')};
}
return null;
}
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.secondary-navigation-tab.pressed.state-layer')};
}
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')};
}
return null;
});
}
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
@override
TabAlignment? get tabAlignment => isScrollable ? TabAlignment.startOffset : TabAlignment.fill;
}
''';
}