Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update ExpansionTile to support Material 3 & add an example #119712

Merged
merged 2 commits into from
Feb 7, 2023
Merged
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
2 changes: 2 additions & 0 deletions dev/tools/gen_defaults/bin/gen_defaults.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import 'package:gen_defaults/color_scheme_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';
Expand Down Expand Up @@ -151,6 +152,7 @@ Future<void> main(List<String> 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();
Expand Down
34 changes: 34 additions & 0 deletions dev/tools/gen_defaults/lib/expansion_tile_template.dart
Original file line number Diff line number Diff line change
@@ -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 => ${textStyle("md.comp.list.list-item.label-text")}!.color;

@override
Color? get iconColor => ${componentColor('md.comp.list.list-item.selected.trailing-icon')};

@override
Color? get collapsedTextColor => ${textStyle("md.comp.list.list-item.label-text")}!.color;

@override
Color? get collapsedIconColor => ${componentColor('md.comp.list.list-item.unselected.trailing-icon')};
}
''';
}
26 changes: 13 additions & 13 deletions examples/api/lib/material/expansion_tile/expansion_tile.0.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Copy link
Contributor

Choose a reason for hiding this comment

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

Please specify
theme: ThemeData(useMaterial3: true),

We shouldn't be demonstrating M2 anymore.

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<MyStatefulWidget> createState() => _MyStatefulWidgetState();
State<ExpansionTileExample> createState() => _ExpansionTileExampleState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
class _ExpansionTileExampleState extends State<ExpansionTileExample> {
bool _customTileExpanded = false;

@override
Expand All @@ -58,7 +56,9 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
ListTile(title: Text('This is tile number 2')),
],
onExpansionChanged: (bool expanded) {
setState(() => _customTileExpanded = expanded);
setState(() {
_customTileExpanded = expanded;
});
},
),
const ExpansionTile(
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
}
86 changes: 76 additions & 10 deletions packages/flutter/lib/src/material/expansion_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -216,7 +217,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:
///
Expand All @@ -227,6 +228,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;


Expand All @@ -235,7 +245,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]
Copy link
Contributor

Choose a reason for hiding this comment

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

Generally speaking, we do not rely on the TextStyle.color of text theme colors. Presumably there's a token that covers what the text color should be (here and elsewhere).

Copy link
Member Author

@TahaTesser TahaTesser Feb 2, 2023

Choose a reason for hiding this comment

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

Since there is no official expansion tile component.
M2 ExpansionTile simply uses ListTile.title's text style color and I did the same here for M3, expasion_tile_template.dart uses the ListTile title token for this color.

cc: @guidezpl
Is there a better way? Can we get another token for this?

Copy link
Member

Choose a reason for hiding this comment

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

Regarding tokens, this is out of spec so there won't be any new ones.

/// will be used for the [title] and [subtitle]. Otherwise, defaults to [ColorScheme.primary] color.
///
/// See also:
///
Expand All @@ -247,8 +258,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:
///
Expand Down Expand Up @@ -441,7 +454,9 @@ class _ExpansionTileState extends State<ExpansionTile> 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
Expand All @@ -458,13 +473,13 @@ class _ExpansionTileState extends State<ExpansionTile> 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;
Expand Down Expand Up @@ -498,3 +513,54 @@ class _ExpansionTileState extends State<ExpansionTile> 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_152

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 => Theme.of(context).textTheme.bodyLarge!.color;

@override
Color? get iconColor => _colors.primary;

@override
Color? get collapsedTextColor => Theme.of(context).textTheme.bodyLarge!.color;

@override
Color? get collapsedIconColor => _colors.onSurface;
}

// END GENERATED TOKEN PROPERTIES - ExpansionTile
63 changes: 63 additions & 0 deletions packages/flutter/test/material/expansion_tile_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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: <Widget>[
SizedBox(height: 100, width: 100),
],
),
),
));

Color getIconColor() => tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color!;
Color getTextColor() => tester.state<TestTextState>(find.byType(TestText)).textStyle.color!;

expect(getIconColor(), theme.colorScheme.onSurface);
expect(getTextColor(), theme.textTheme.bodyLarge!.color);

await tester.tap(find.text('title'));
await tester.pumpAndSettle();

expect(getIconColor(), theme.colorScheme.primary);
expect(getTextColor(), theme.textTheme.bodyLarge!.color);
});

testWidgets('ExpansionTile iconColor, textColor', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/pull/78281

Expand Down Expand Up @@ -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: <Widget>[
SizedBox(height: 100, width: 100),
],
),
),
));

Color getIconColor() => tester.state<TestIconState>(find.byType(TestIcon)).iconTheme.color!;
Color getTextColor() => tester.state<TestTextState>(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);
});
});
}