diff --git a/dev/tools/gen_defaults/bin/gen_defaults.dart b/dev/tools/gen_defaults/bin/gen_defaults.dart index 71eb69a09ff..44b1fc3350f 100644 --- a/dev/tools/gen_defaults/bin/gen_defaults.dart +++ b/dev/tools/gen_defaults/bin/gen_defaults.dart @@ -31,6 +31,7 @@ import 'package:gen_defaults/date_picker_template.dart'; import 'package:gen_defaults/dialog_template.dart'; import 'package:gen_defaults/divider_template.dart'; import 'package:gen_defaults/drawer_template.dart'; +import 'package:gen_defaults/expansion_tile_template.dart'; import 'package:gen_defaults/fab_template.dart'; import 'package:gen_defaults/filter_chip_template.dart'; import 'package:gen_defaults/icon_button_template.dart'; @@ -153,6 +154,7 @@ Future main(List args) async { DialogTemplate('Dialog', '$materialLib/dialog.dart', tokens).updateFile(); DividerTemplate('Divider', '$materialLib/divider.dart', tokens).updateFile(); DrawerTemplate('Drawer', '$materialLib/drawer.dart', tokens).updateFile(); + ExpansionTileTemplate('ExpansionTile', '$materialLib/expansion_tile.dart', tokens).updateFile(); FABTemplate('FAB', '$materialLib/floating_action_button.dart', tokens).updateFile(); FilterChipTemplate('ChoiceChip', '$materialLib/choice_chip.dart', tokens).updateFile(); FilterChipTemplate('FilterChip', '$materialLib/filter_chip.dart', tokens).updateFile(); diff --git a/dev/tools/gen_defaults/lib/expansion_tile_template.dart b/dev/tools/gen_defaults/lib/expansion_tile_template.dart new file mode 100644 index 00000000000..e2f19658263 --- /dev/null +++ b/dev/tools/gen_defaults/lib/expansion_tile_template.dart @@ -0,0 +1,34 @@ +// 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 ExpansionTileTemplate extends TokenTemplate { + const ExpansionTileTemplate(super.blockName, super.fileName, super.tokens, { + super.colorSchemePrefix = '_colors.', + }); + + @override + String generate() => ''' +class _${blockName}DefaultsM3 extends ExpansionTileThemeData { + _${blockName}DefaultsM3(this.context); + + final BuildContext context; + late final ThemeData _theme = Theme.of(context); + late final ColorScheme _colors = _theme.colorScheme; + + @override + Color? get textColor => ${componentColor('md.comp.list.list-item.label-text')}; + + @override + Color? get iconColor => ${componentColor('md.comp.list.list-item.selected.trailing-icon')}; + + @override + Color? get collapsedTextColor => ${componentColor('md.comp.list.list-item.label-text')}; + + @override + Color? get collapsedIconColor => ${componentColor('md.comp.list.list-item.trailing-icon')}; +} +'''; +} diff --git a/examples/api/lib/material/expansion_tile/expansion_tile.0.dart b/examples/api/lib/material/expansion_tile/expansion_tile.0.dart index 711b9e35428..6a2651430ce 100644 --- a/examples/api/lib/material/expansion_tile/expansion_tile.0.dart +++ b/examples/api/lib/material/expansion_tile/expansion_tile.0.dart @@ -6,33 +6,31 @@ import 'package:flutter/material.dart'; -void main() => runApp(const MyApp()); +void main() => runApp(const ExpansionTileApp()); -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - static const String _title = 'Flutter Code Sample'; +class ExpansionTileApp extends StatelessWidget { + const ExpansionTileApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( - title: _title, + theme: ThemeData(useMaterial3: true), home: Scaffold( - appBar: AppBar(title: const Text(_title)), - body: const MyStatefulWidget(), + appBar: AppBar(title: const Text('ExpansionTile Sample')), + body: const ExpansionTileExample(), ), ); } } -class MyStatefulWidget extends StatefulWidget { - const MyStatefulWidget({super.key}); +class ExpansionTileExample extends StatefulWidget { + const ExpansionTileExample({super.key}); @override - State createState() => _MyStatefulWidgetState(); + State createState() => _ExpansionTileExampleState(); } -class _MyStatefulWidgetState extends State { +class _ExpansionTileExampleState extends State { bool _customTileExpanded = false; @override @@ -51,14 +49,16 @@ class _MyStatefulWidgetState extends State { subtitle: const Text('Custom expansion arrow icon'), trailing: Icon( _customTileExpanded - ? Icons.arrow_drop_down_circle - : Icons.arrow_drop_down, + ? Icons.arrow_drop_down_circle + : Icons.arrow_drop_down, ), children: const [ ListTile(title: Text('This is tile number 2')), ], onExpansionChanged: (bool expanded) { - setState(() => _customTileExpanded = expanded); + setState(() { + _customTileExpanded = expanded; + }); }, ), const ExpansionTile( diff --git a/examples/api/test/material/expansion_tile/expansion_tile.0_test.dart b/examples/api/test/material/expansion_tile/expansion_tile.0_test.dart new file mode 100644 index 00000000000..75c3246add9 --- /dev/null +++ b/examples/api/test/material/expansion_tile/expansion_tile.0_test.dart @@ -0,0 +1,40 @@ +// 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 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/expansion_tile/expansion_tile.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('When expansion tiles are expanded tile numbers are revealed', (WidgetTester tester) async { + const int totalTiles = 3; + + await tester.pumpWidget( + const example.ExpansionTileApp(), + ); + + expect(find.byType(ExpansionTile), findsNWidgets(totalTiles)); + + const String tileOne = 'This is tile number 1'; + expect(find.text(tileOne), findsNothing); + + await tester.tap(find.text('ExpansionTile 1')); + await tester.pumpAndSettle(); + expect(find.text(tileOne), findsOneWidget); + + const String tileTwo = 'This is tile number 2'; + expect(find.text(tileTwo), findsNothing); + + await tester.tap(find.text('ExpansionTile 2')); + await tester.pumpAndSettle(); + expect(find.text(tileTwo), findsOneWidget); + + const String tileThree = 'This is tile number 3'; + expect(find.text(tileThree), findsNothing); + + await tester.tap(find.text('ExpansionTile 3')); + await tester.pumpAndSettle(); + expect(find.text(tileThree), findsOneWidget); + }); +} diff --git a/packages/flutter/lib/src/material/expansion_tile.dart b/packages/flutter/lib/src/material/expansion_tile.dart index 0f19918e7cd..2b104f4917e 100644 --- a/packages/flutter/lib/src/material/expansion_tile.dart +++ b/packages/flutter/lib/src/material/expansion_tile.dart @@ -34,7 +34,8 @@ const Duration _kExpand = Duration(milliseconds: 200); /// to the [leading] and [trailing] properties of [ExpansionTile]. /// /// {@tool dartpad} -/// This example demonstrates different configurations of ExpansionTile. +/// This example demonstrates how the [ExpansionTile] icon's location and appearance +/// can be customized. /// /// ** See code in examples/api/lib/material/expansion_tile/expansion_tile.0.dart ** /// {@end-tool} @@ -218,7 +219,7 @@ class ExpansionTile extends StatefulWidget { /// Used to override to the [ListTileThemeData.iconColor]. /// /// If this property is null then [ExpansionTileThemeData.iconColor] is used. If that - /// is also null then the value of [ListTileThemeData.iconColor] is used. + /// is also null then the value of [ColorScheme.primary] is used. /// /// See also: /// @@ -229,6 +230,15 @@ class ExpansionTile extends StatefulWidget { /// The icon color of tile's expansion arrow icon when the sublist is collapsed. /// /// Used to override to the [ListTileThemeData.iconColor]. + /// + /// If this property is null then [ExpansionTileThemeData.collapsedIconColor] is used. If that + /// is also null and [ThemeData.useMaterial3] is true, [ColorScheme.onSurface] is used. Otherwise, + /// defaults to [ThemeData.unselectedWidgetColor] color. + /// + /// See also: + /// + /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s + /// [ExpansionTileThemeData]. final Color? collapsedIconColor; @@ -237,7 +247,8 @@ class ExpansionTile extends StatefulWidget { /// Used to override to the [ListTileThemeData.textColor]. /// /// If this property is null then [ExpansionTileThemeData.textColor] is used. If that - /// is also null then the value of [ListTileThemeData.textColor] is used. + /// is also null then and [ThemeData.useMaterial3] is true, color of the [TextTheme.bodyLarge] + /// will be used for the [title] and [subtitle]. Otherwise, defaults to [ColorScheme.primary] color. /// /// See also: /// @@ -249,8 +260,10 @@ class ExpansionTile extends StatefulWidget { /// /// Used to override to the [ListTileThemeData.textColor]. /// - /// If this property is null then [ExpansionTileThemeData.collapsedTextColor] is used. If that - /// is also null then the value of [ListTileThemeData.textColor] is used. + /// If this property is null then [ExpansionTileThemeData.collapsedTextColor] is used. + /// If that is also null and [ThemeData.useMaterial3] is true, color of the + /// [TextTheme.bodyLarge] will be used for the [title] and [subtitle]. Otherwise, + /// defaults to color of the [TextTheme.titleMedium]. /// /// See also: /// @@ -443,7 +456,9 @@ class _ExpansionTileState extends State with SingleTickerProvider void didChangeDependencies() { final ThemeData theme = Theme.of(context); final ExpansionTileThemeData expansionTileTheme = ExpansionTileTheme.of(context); - final ColorScheme colorScheme = theme.colorScheme; + final ExpansionTileThemeData defaults = theme.useMaterial3 + ? _ExpansionTileDefaultsM3(context) + : _ExpansionTileDefaultsM2(context); _borderTween ..begin = widget.collapsedShape ?? expansionTileTheme.collapsedShape @@ -460,13 +475,13 @@ class _ExpansionTileState extends State with SingleTickerProvider _headerColorTween ..begin = widget.collapsedTextColor ?? expansionTileTheme.collapsedTextColor - ?? theme.textTheme.titleMedium!.color - ..end = widget.textColor ?? expansionTileTheme.textColor ?? colorScheme.primary; + ?? defaults.collapsedTextColor + ..end = widget.textColor ?? expansionTileTheme.textColor ?? defaults.textColor; _iconColorTween ..begin = widget.collapsedIconColor ?? expansionTileTheme.collapsedIconColor - ?? theme.unselectedWidgetColor - ..end = widget.iconColor ?? expansionTileTheme.iconColor ?? colorScheme.primary; + ?? defaults.collapsedIconColor + ..end = widget.iconColor ?? expansionTileTheme.iconColor ?? defaults.iconColor; _backgroundColorTween ..begin = widget.collapsedBackgroundColor ?? expansionTileTheme.collapsedBackgroundColor ..end = widget.backgroundColor ?? expansionTileTheme.backgroundColor; @@ -500,3 +515,54 @@ class _ExpansionTileState extends State with SingleTickerProvider ); } } + +class _ExpansionTileDefaultsM2 extends ExpansionTileThemeData { + _ExpansionTileDefaultsM2(this.context); + + final BuildContext context; + late final ThemeData _theme = Theme.of(context); + late final ColorScheme _colorScheme = _theme.colorScheme; + + @override + Color? get textColor => _colorScheme.primary; + + @override + Color? get iconColor => _colorScheme.primary; + + @override + Color? get collapsedTextColor => _theme.textTheme.titleMedium!.color; + + @override + Color? get collapsedIconColor => _theme.unselectedWidgetColor; +} + +// BEGIN GENERATED TOKEN PROPERTIES - ExpansionTile + +// Do not edit by hand. The code between the "BEGIN GENERATED" and +// "END GENERATED" comments are generated from data in the Material +// Design token database by the script: +// dev/tools/gen_defaults/bin/gen_defaults.dart. + +// Token database version: v0_158 + +class _ExpansionTileDefaultsM3 extends ExpansionTileThemeData { + _ExpansionTileDefaultsM3(this.context); + + final BuildContext context; + late final ThemeData _theme = Theme.of(context); + late final ColorScheme _colors = _theme.colorScheme; + + @override + Color? get textColor => _colors.onSurface; + + @override + Color? get iconColor => _colors.primary; + + @override + Color? get collapsedTextColor => _colors.onSurface; + + @override + Color? get collapsedIconColor => _colors.onSurfaceVariant; +} + +// END GENERATED TOKEN PROPERTIES - ExpansionTile diff --git a/packages/flutter/test/material/expansion_tile_test.dart b/packages/flutter/test/material/expansion_tile_test.dart index 7c4d54826f2..86e32b62800 100644 --- a/packages/flutter/test/material/expansion_tile_test.dart +++ b/packages/flutter/test/material/expansion_tile_test.dart @@ -522,6 +522,35 @@ void main() { expect(shapeDecoration.color, backgroundColor); }); + testWidgets('ExpansionTile default iconColor, textColor', (WidgetTester tester) async { + final ThemeData theme = ThemeData(useMaterial3: true); + + await tester.pumpWidget(MaterialApp( + theme: theme, + home: const Material( + child: ExpansionTile( + title: TestText('title'), + trailing: TestIcon(), + children: [ + SizedBox(height: 100, width: 100), + ], + ), + ), + )); + + Color getIconColor() => tester.state(find.byType(TestIcon)).iconTheme.color!; + Color getTextColor() => tester.state(find.byType(TestText)).textStyle.color!; + + expect(getIconColor(), theme.colorScheme.onSurfaceVariant); + expect(getTextColor(), theme.colorScheme.onSurface); + + await tester.tap(find.text('title')); + await tester.pumpAndSettle(); + + expect(getIconColor(), theme.colorScheme.primary); + expect(getTextColor(), theme.colorScheme.onSurface); + }); + testWidgets('ExpansionTile iconColor, textColor', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/pull/78281 @@ -666,4 +695,38 @@ void main() { expect(listTile.leading.runtimeType, Icon); expect(listTile.trailing, isNull); }); + + 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('ExpansionTile default iconColor, textColor', (WidgetTester tester) async { + final ThemeData theme = ThemeData(useMaterial3: false); + + await tester.pumpWidget(MaterialApp( + theme: theme, + home: const Material( + child: ExpansionTile( + title: TestText('title'), + trailing: TestIcon(), + children: [ + SizedBox(height: 100, width: 100), + ], + ), + ), + )); + + Color getIconColor() => tester.state(find.byType(TestIcon)).iconTheme.color!; + Color getTextColor() => tester.state(find.byType(TestText)).textStyle.color!; + + expect(getIconColor(), theme.unselectedWidgetColor); + expect(getTextColor(), theme.textTheme.titleMedium!.color); + + await tester.tap(find.text('title')); + await tester.pumpAndSettle(); + + expect(getIconColor(), theme.colorScheme.primary); + expect(getTextColor(), theme.colorScheme.primary); + }); + }); }