Add ActionButtonIconsData for overriding action icons (#118229)

* Add ActionButtonIconsData for overriding action icons

* Fix formatting issues

* Add missing exports in material library and add copyWith method in ActionButtonIconsData

* Move all action buttons, and icons to action_buttons.dart

* Rename actionButtonIcons to actionIconTheme

* Refactor buttons in action_buttons.dart to extend a private class for common implementation

* Refactor icons in action_buttons

* Fix docs in action_buttons_theme

* Fix #107646 always use 'Icons.arrow_back' as a back_button icon in web

* Update documentation for action buttons and add style parameter to every action button

* Fix analyzer warnings

* Add missing style argument in IconButton of _ActionButton

* Add tests for action buttons, action icon theme, drawer buttons, and back buttons

* Add example (+test) for action icon button's action icon theme in examples/api

* Fix analysis errors

* Add missing license header in action_icon_theme.0.dart

* Fix deprecation notice in theme_data.dart

* Update theme data tests for actionIconTheme

* Remove iconSize parameter from ActionButtons and update docs

* Fix failing tests

* Update button color during backbutton tests to red

* Fix analytics issues

* Fix format
This commit is contained in:
Mushaheed Syed 2023-02-22 22:52:44 +05:30 committed by GitHub
parent d816e72f07
commit 7d85a585da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 1321 additions and 220 deletions

View file

@ -0,0 +1,116 @@
// 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.
// Flutter code sample for [ActionIconTheme].
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class _CustomEndDrawerIcon extends StatelessWidget {
const _CustomEndDrawerIcon();
@override
Widget build(BuildContext context) {
final MaterialLocalizations localization = MaterialLocalizations.of(context);
return Icon(
Icons.more_horiz,
semanticLabel: localization.openAppDrawerTooltip,
);
}
}
class _CustomDrawerIcon extends StatelessWidget {
const _CustomDrawerIcon();
@override
Widget build(BuildContext context) {
final MaterialLocalizations localization = MaterialLocalizations.of(context);
return Icon(
Icons.segment,
semanticLabel: localization.openAppDrawerTooltip,
);
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
actionIconTheme: ActionIconThemeData(
backButtonIconBuilder: (BuildContext context) {
return const Icon(Icons.arrow_back_ios_new_rounded);
},
drawerButtonIconBuilder: (BuildContext context) {
return const _CustomDrawerIcon();
},
endDrawerButtonIconBuilder: (BuildContext context) {
return const _CustomEndDrawerIcon();
},
),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
drawer: const Drawer(),
body: const Center(
child: NextPageButton(),
),
);
}
}
class NextPageButton extends StatelessWidget {
const NextPageButton({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton.icon(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<MySecondPage>(builder: (BuildContext context) {
return const MySecondPage();
}),
);
},
icon: const Icon(Icons.arrow_forward),
label: const Text('Next page'),
);
}
}
class MySecondPage extends StatelessWidget {
const MySecondPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Second page'),
),
endDrawer: const Drawer(),
);
}
}

View file

@ -0,0 +1,51 @@
// 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/action_buttons/action_icon_theme.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Action Icon Buttons', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: example.MyApp(),
),
),
);
expect(find.byType(DrawerButton), findsOneWidget);
final Icon drawerButtonIcon = tester.widget(
find.descendant(
of: find.byType(DrawerButton),
matching: find.byType(Icon),
),
);
expect(drawerButtonIcon.icon, Icons.segment);
// open next page
await tester.tap(find.byType(example.NextPageButton));
await tester.pumpAndSettle();
expect(find.byType(EndDrawerButton), findsOneWidget);
final Icon endDrawerButtonIcon = tester.widget(
find.descendant(
of: find.byType(EndDrawerButton),
matching: find.byType(Icon),
),
);
expect(endDrawerButtonIcon.icon, Icons.more_horiz);
expect(find.byType(BackButton), findsOneWidget);
final Icon backButtonIcon = tester.widget(
find.descendant(
of: find.byType(BackButton),
matching: find.byType(Icon),
),
);
expect(backButtonIcon.icon, Icons.arrow_back_ios_new_rounded);
});
}

View file

@ -21,7 +21,9 @@
library material;
export 'src/material/about.dart';
export 'src/material/action_buttons.dart';
export 'src/material/action_chip.dart';
export 'src/material/action_icons_theme.dart';
export 'src/material/adaptive_text_selection_toolbar.dart';
export 'src/material/animated_icons.dart';
export 'src/material/app.dart';
@ -29,7 +31,6 @@ export 'src/material/app_bar.dart';
export 'src/material/app_bar_theme.dart';
export 'src/material/arc.dart';
export 'src/material/autocomplete.dart';
export 'src/material/back_button.dart';
export 'src/material/badge.dart';
export 'src/material/badge_theme.dart';
export 'src/material/banner.dart';

View file

@ -0,0 +1,418 @@
// 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/foundation.dart';
import 'package:flutter/widgets.dart';
import 'action_icons_theme.dart';
import 'button_style.dart';
import 'debug.dart';
import 'icon_button.dart';
import 'icons.dart';
import 'material_localizations.dart';
import 'scaffold.dart';
import 'theme.dart';
abstract class _ActionButton extends StatelessWidget {
/// Creates a Material Design icon button.
const _ActionButton({
super.key,
this.color,
required this.icon,
required this.onPressed,
this.style,
});
/// The icon to display inside the button.
final Widget icon;
/// The callback that is called when the button is tapped
/// or otherwise activated.
///
/// If this is set to null, the button will do a default action
/// when it is tapped or activated.
final VoidCallback? onPressed;
/// The color to use for the icon.
///
/// Defaults to the [IconThemeData.color] specified in the ambient [IconTheme],
/// which usually matches the ambient [Theme]'s [ThemeData.iconTheme].
final Color? color;
/// Customizes this icon button's appearance.
///
/// The [style] is only used for Material 3 [IconButton]s. If [ThemeData.useMaterial3]
/// is set to true, [style] is preferred for icon button customization, and any
/// parameters defined in [style] will override the same parameters in [IconButton].
///
/// Null by default.
final ButtonStyle? style;
/// This returns the appropriate tooltip text for this action button.
String _getTooltip(BuildContext context);
/// This is the default function that is called when [onPressed] is set
/// to null.
void _onPressedCallback(BuildContext context);
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
return IconButton(
icon: icon,
style: style,
color: color,
tooltip: _getTooltip(context),
onPressed: () {
if (onPressed != null) {
onPressed!();
} else {
_onPressedCallback(context);
}
},
);
}
}
typedef _ActionIconBuilderCallback = WidgetBuilder? Function(ActionIconThemeData? actionIconTheme);
typedef _ActionIconDataCallback = IconData Function(BuildContext context);
typedef _AndroidSemanticsLabelCallback = String Function(MaterialLocalizations materialLocalization);
class _ActionIcon extends StatelessWidget {
const _ActionIcon({
required this.iconBuilderCallback,
required this.getIcon,
required this.getAndroidSemanticsLabel,
});
final _ActionIconBuilderCallback iconBuilderCallback;
final _ActionIconDataCallback getIcon;
final _AndroidSemanticsLabelCallback getAndroidSemanticsLabel;
@override
Widget build(BuildContext context) {
final ActionIconThemeData? actionIconTheme = ActionIconTheme.of(context);
final WidgetBuilder? iconBuilder = iconBuilderCallback(actionIconTheme);
if (iconBuilder != null) {
return iconBuilder(context);
}
final IconData data = getIcon(context);
final String? semanticsLabel;
// This can't use the platform from Theme because it is the Android OS that
// expects the duplicated tooltip and label.
switch (defaultTargetPlatform) {
case TargetPlatform.android:
semanticsLabel = getAndroidSemanticsLabel(MaterialLocalizations.of(context));
break;
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
case TargetPlatform.iOS:
case TargetPlatform.macOS:
semanticsLabel = null;
break;
}
return Icon(data, semanticLabel: semanticsLabel);
}
}
/// A "back" icon that's appropriate for the current [TargetPlatform].
///
/// The current platform is determined by querying for the ambient [Theme].
///
/// See also:
///
/// * [BackButton], an [IconButton] with a [BackButtonIcon] that calls
/// [Navigator.maybePop] to return to the previous route.
/// * [IconButton], which is a more general widget for creating buttons
/// with icons.
/// * [Icon], a Material Design icon.
/// * [ThemeData.platform], which specifies the current platform.
class BackButtonIcon extends StatelessWidget {
/// Creates an icon that shows the appropriate "back" image for
/// the current platform (as obtained from the [Theme]).
const BackButtonIcon({ super.key });
@override
Widget build(BuildContext context) {
return _ActionIcon(
iconBuilderCallback: (ActionIconThemeData? actionIconTheme) {
return actionIconTheme?.backButtonIconBuilder;
},
getIcon: (BuildContext context) {
if (kIsWeb) {
// Always use 'Icons.arrow_back' as a back_button icon in web.
return Icons.arrow_back;
}
switch (Theme.of(context).platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return Icons.arrow_back;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return Icons.arrow_back_ios;
}
},
getAndroidSemanticsLabel: (MaterialLocalizations materialLocalization) {
return materialLocalization.backButtonTooltip;
},
);
}
}
/// A Material Design back icon button.
///
/// A [BackButton] is an [IconButton] with a "back" icon appropriate for the
/// current [TargetPlatform]. When pressed, the back button calls
/// [Navigator.maybePop] to return to the previous route unless a custom
/// [onPressed] callback is provided.
///
/// The [onPressed] callback can, for instance, be used to pop the platform's navigation stack
/// via [SystemNavigator] instead of Flutter's [Navigator] in add-to-app
/// situations.
///
/// In Material Design 3, both [style]'s [ButtonStyle.iconColor] and [color] are
/// used to override the default icon color of [BackButton]. If both exist, the [ButtonStyle.iconColor]
/// will override [color] for states where [ButtonStyle.foregroundColor] resolves to non-null.
///
/// When deciding to display a [BackButton], consider using
/// `ModalRoute.of(context)?.canPop` to check whether the current route can be
/// popped. If that value is false (e.g., because the current route is the
/// initial route), the [BackButton] will not have any effect when pressed,
/// which could frustrate the user.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
///
/// * [AppBar], which automatically uses a [BackButton] in its
/// [AppBar.leading] slot when the [Scaffold] has no [Drawer] and the
/// current [Route] is not the [Navigator]'s first route.
/// * [BackButtonIcon], which is useful if you need to create a back button
/// that responds differently to being pressed.
/// * [IconButton], which is a more general widget for creating buttons with
/// icons.
/// * [CloseButton], an alternative which may be more appropriate for leaf
/// node pages in the navigation tree.
class BackButton extends _ActionButton {
/// Creates an [IconButton] with the appropriate "back" icon for the current
/// target platform.
const BackButton({
super.key,
super.color,
super.style,
super.onPressed,
}) : super(icon: const BackButtonIcon());
@override
void _onPressedCallback(BuildContext context) => Navigator.maybePop(context);
@override
String _getTooltip(BuildContext context) {
return MaterialLocalizations.of(context).backButtonTooltip;
}
}
/// A "close" icon that's appropriate for the current [TargetPlatform].
///
/// The current platform is determined by querying for the ambient [Theme].
///
/// See also:
///
/// * [CloseButton], an [IconButton] with a [CloseButtonIcon] that calls
/// [Navigator.maybePop] to return to the previous route.
/// * [IconButton], which is a more general widget for creating buttons
/// with icons.
/// * [Icon], a Material Design icon.
/// * [ThemeData.platform], which specifies the current platform.
class CloseButtonIcon extends StatelessWidget {
/// Creates an icon that shows the appropriate "close" image for
/// the current platform (as obtained from the [Theme]).
const CloseButtonIcon({ super.key });
@override
Widget build(BuildContext context) {
return _ActionIcon(
iconBuilderCallback: (ActionIconThemeData? actionIconTheme) {
return actionIconTheme?.closeButtonIconBuilder;
},
getIcon: (BuildContext context) => Icons.close,
getAndroidSemanticsLabel: (MaterialLocalizations materialLocalization) {
return materialLocalization.closeButtonTooltip;
},
);
}
}
/// A Material Design close icon button.
///
/// A [CloseButton] is an [IconButton] with a "close" icon. When pressed, the
/// close button calls [Navigator.maybePop] to return to the previous route.
///
/// The [onPressed] callback can, for instance, be used to pop the platform's navigation stack
/// via [SystemNavigator] instead of Flutter's [Navigator] in add-to-app
/// situations.
///
/// In Material Design 3, both [style]'s [ButtonStyle.iconColor] and [color] are
/// used to override the default icon color of [CloseButton]. If both exist, the [ButtonStyle.iconColor]
/// will override [color] for states where [ButtonStyle.foregroundColor] resolves to non-null.
///
/// Use a [CloseButton] instead of a [BackButton] on fullscreen dialogs or
/// pages that may solicit additional actions to close.
///
/// See also:
///
/// * [AppBar], which automatically uses a [CloseButton] in its
/// [AppBar.leading] slot when appropriate.
/// * [BackButton], which is more appropriate for middle nodes in the
/// navigation tree or where pages can be popped instantaneously with
/// no user data consequence.
/// * [IconButton], to create other Material Design icon buttons.
class CloseButton extends _ActionButton {
/// Creates a Material Design close icon button.
const CloseButton({ super.key, super.color, super.onPressed, super.style })
: super(icon: const CloseButtonIcon());
@override
void _onPressedCallback(BuildContext context) => Navigator.maybePop(context);
@override
String _getTooltip(BuildContext context) {
return MaterialLocalizations.of(context).closeButtonTooltip;
}
}
/// A "drawer" icon that's appropriate for the current [TargetPlatform].
///
/// The current platform is determined by querying for the ambient [Theme].
///
/// See also:
///
/// * [DrawerButton], an [IconButton] with a [DrawerButtonIcon] that calls
/// [ScaffoldState.openDrawer] to open the [Scaffold.drawer].
/// * [EndDrawerButton], an [IconButton] with an [EndDrawerButtonIcon] that
/// calls [ScaffoldState.openEndDrawer] to open the [Scaffold.endDrawer].
/// * [IconButton], which is a more general widget for creating buttons
/// with icons.
/// * [Icon], a Material Design icon.
/// * [ThemeData.platform], which specifies the current platform.
class DrawerButtonIcon extends StatelessWidget {
/// Creates an icon that shows the appropriate "close" image for
/// the current platform (as obtained from the [Theme]).
const DrawerButtonIcon({ super.key });
@override
Widget build(BuildContext context) {
return _ActionIcon(
iconBuilderCallback: (ActionIconThemeData? actionIconTheme) {
return actionIconTheme?.drawerButtonIconBuilder;
},
getIcon: (BuildContext context) => Icons.menu,
getAndroidSemanticsLabel: (MaterialLocalizations materialLocalization) {
return materialLocalization.openAppDrawerTooltip;
},
);
}
}
/// A Material Design drawer icon button.
///
/// A [DrawerButton] is an [IconButton] with a "drawer" icon. When pressed, the
/// close button calls [ScaffoldState.openDrawer] to the [Scaffold.drawer].
///
/// The default behaviour on press can be overriden with [onPressed].
///
/// See also:
///
/// * [EndDrawerButton], an [IconButton] with an [EndDrawerButtonIcon] that
/// calls [ScaffoldState.openEndDrawer] to open the [Scaffold.endDrawer].
/// * [IconButton], which is a more general widget for creating buttons
/// with icons.
/// * [Icon], a Material Design icon.
/// * [ThemeData.platform], which specifies the current platform.
class DrawerButton extends _ActionButton {
/// Creates a Material Design drawer icon button.
const DrawerButton({
super.key,
super.style,
super.onPressed,
}) : super(icon: const DrawerButtonIcon());
@override
void _onPressedCallback(BuildContext context) => Scaffold.of(context).openDrawer();
@override
String _getTooltip(BuildContext context) {
return MaterialLocalizations.of(context).openAppDrawerTooltip;
}
}
/// A "end drawer" icon that's appropriate for the current [TargetPlatform].
///
/// The current platform is determined by querying for the ambient [Theme].
///
/// See also:
///
/// * [DrawerButton], an [IconButton] with a [DrawerButtonIcon] that calls
/// [ScaffoldState.openDrawer] to open the [Scaffold.drawer].
/// * [EndDrawerButton], an [IconButton] with an [EndDrawerButtonIcon] that
/// calls [ScaffoldState.openEndDrawer] to open the [Scaffold.endDrawer]
/// * [IconButton], which is a more general widget for creating buttons
/// with icons.
/// * [Icon], a Material Design icon.
/// * [ThemeData.platform], which specifies the current platform.
class EndDrawerButtonIcon extends StatelessWidget {
/// Creates an icon that shows the appropriate "end drawer" image for
/// the current platform (as obtained from the [Theme]).
const EndDrawerButtonIcon({ super.key });
@override
Widget build(BuildContext context) {
return _ActionIcon(
iconBuilderCallback: (ActionIconThemeData? actionIconTheme) {
return actionIconTheme?.endDrawerButtonIconBuilder;
},
getIcon: (BuildContext context) => Icons.menu,
getAndroidSemanticsLabel: (MaterialLocalizations materialLocalization) {
return materialLocalization.openAppDrawerTooltip;
},
);
}
}
/// A Material Design end drawer icon button.
///
/// A [EndDrawerButton] is an [IconButton] with a "drawer" icon. When pressed, the
/// end drawer button calls [ScaffoldState.openEndDrawer] to open the [Scaffold.endDrawer].
///
/// The default behaviour on press can be overriden with [onPressed].
///
/// See also:
///
/// * [DrawerButton], an [IconButton] with a [DrawerButtonIcon] that calls
/// [ScaffoldState.openDrawer] to open a drawer.
/// * [IconButton], which is a more general widget for creating buttons
/// with icons.
/// * [Icon], a Material Design icon.
/// * [ThemeData.platform], which specifies the current platform.
class EndDrawerButton extends _ActionButton {
/// Creates a Material Design end drawer icon button.
const EndDrawerButton({
super.key,
super.style,
super.onPressed,
}) : super(icon: const EndDrawerButtonIcon());
@override
void _onPressedCallback(BuildContext context) => Scaffold.of(context).openEndDrawer();
@override
String _getTooltip(BuildContext context) {
return MaterialLocalizations.of(context).openAppDrawerTooltip;
}
}

View file

@ -0,0 +1,153 @@
// 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/foundation.dart';
import 'package:flutter/widgets.dart';
import 'action_buttons.dart';
import 'theme.dart';
// Examples can assume:
// late BuildContext context;
/// A [ActionIconThemeData] that overrides the default icons of
/// [BackButton], [CloseButton], [DrawerButton], and [EndDrawerButton] with
/// [ActionIconTheme.of] or the overall [Theme]'s [ThemeData.actionIconTheme].
@immutable
class ActionIconThemeData with Diagnosticable {
/// Creates an [ActionIconThemeData].
///
/// The builders [backButtonIconBuilder], [closeButtonIconBuilder],
/// [drawerButtonIconBuilder], [endDrawerButtonIconBuilder] may be null.
const ActionIconThemeData({ this.backButtonIconBuilder, this.closeButtonIconBuilder, this.drawerButtonIconBuilder, this.endDrawerButtonIconBuilder });
/// Overrides [BackButtonIcon]'s icon.
///
/// If [backButtonIconBuilder] is null, then [BackButtonIcon]
/// fallbacks to the platform's default back button icon.
final WidgetBuilder? backButtonIconBuilder;
/// Overrides [CloseButtonIcon]'s icon.
///
/// If [closeButtonIconBuilder] is null, then [CloseButtonIcon]
/// fallbacks to the platform's default close button icon.
final WidgetBuilder? closeButtonIconBuilder;
/// Overrides [DrawerButtonIcon]'s icon.
///
/// If [drawerButtonIconBuilder] is null, then [DrawerButtonIcon]
/// fallbacks to the platform's default drawer button icon.
final WidgetBuilder? drawerButtonIconBuilder;
/// Overrides [EndDrawerButtonIcon]'s icon.
///
/// If [endDrawerButtonIconBuilder] is null, then [EndDrawerButtonIcon]
/// fallbacks to the platform's default end drawer button icon.
final WidgetBuilder? endDrawerButtonIconBuilder;
/// Creates a copy of this object but with the given fields replaced with the
/// new values.
ActionIconThemeData copyWith({
WidgetBuilder? backButtonIconBuilder,
WidgetBuilder? closeButtonIconBuilder,
WidgetBuilder? drawerButtonIconBuilder,
WidgetBuilder? endDrawerButtonIconBuilder,
}) {
return ActionIconThemeData(
backButtonIconBuilder: backButtonIconBuilder ?? backButtonIconBuilder,
closeButtonIconBuilder: closeButtonIconBuilder ?? closeButtonIconBuilder,
drawerButtonIconBuilder: drawerButtonIconBuilder ?? drawerButtonIconBuilder,
endDrawerButtonIconBuilder: endDrawerButtonIconBuilder ?? endDrawerButtonIconBuilder,
);
}
/// Linearly interpolate between two action icon themes.
static ActionIconThemeData? lerp(ActionIconThemeData? a, ActionIconThemeData? b, double t) {
if (a == null && b == null) {
return null;
}
return ActionIconThemeData(
backButtonIconBuilder: t < 0.5 ? a?.backButtonIconBuilder : b?.backButtonIconBuilder,
closeButtonIconBuilder: t < 0.5 ? a?.closeButtonIconBuilder : b?.closeButtonIconBuilder,
drawerButtonIconBuilder: t < 0.5 ? a?.drawerButtonIconBuilder : b?.drawerButtonIconBuilder,
endDrawerButtonIconBuilder: t < 0.5 ? a?.endDrawerButtonIconBuilder : b?.endDrawerButtonIconBuilder,
);
}
@override
int get hashCode {
final List<Object?> values = <Object?>[
backButtonIconBuilder,
closeButtonIconBuilder,
drawerButtonIconBuilder,
endDrawerButtonIconBuilder,
];
return Object.hashAll(values);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is ActionIconThemeData
&& other.backButtonIconBuilder == backButtonIconBuilder
&& other.closeButtonIconBuilder == closeButtonIconBuilder
&& other.drawerButtonIconBuilder == drawerButtonIconBuilder
&& other.endDrawerButtonIconBuilder == endDrawerButtonIconBuilder;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<WidgetBuilder>('backButtonIconBuilder', backButtonIconBuilder, defaultValue: null));
properties.add(DiagnosticsProperty<WidgetBuilder>('closeButtonIconBuilder', closeButtonIconBuilder, defaultValue: null));
properties.add(DiagnosticsProperty<WidgetBuilder>('drawerButtonIconBuilder', drawerButtonIconBuilder, defaultValue: null));
properties.add(DiagnosticsProperty<WidgetBuilder>('endDrawerButtonIconBuilder', endDrawerButtonIconBuilder, defaultValue: null));
}
}
/// An inherited widget that overrides the default icon of [BackButtonIcon],
/// [CloseButtonIcon], [DrawerButtonIcon], and [EndDrawerButtonIcon] in this
/// widget's subtree.
class ActionIconTheme extends InheritedTheme {
/// Creates a theme that overrides the default icon of [BackButtonIcon],
/// [CloseButtonIcon], [DrawerButtonIcon], and [EndDrawerButtonIcon] in this
/// widget's subtree.
const ActionIconTheme({
super.key,
required this.data,
required super.child,
});
/// Specifies the default icon overrides for descendant [BackButtonIcon],
/// [CloseButtonIcon], [DrawerButtonIcon], and [EndDrawerButtonIcon] widgets.
final ActionIconThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [ActionIconTheme] widget, then
/// [ThemeData.actionIconTheme] is used.
///
/// Typical usage is as follows:
///
/// ```dart
/// ActionIconThemeData? theme = ActionIconTheme.of(context);
/// ```
static ActionIconThemeData? of(BuildContext context) {
final ActionIconTheme? actionIconTheme = context.dependOnInheritedWidgetOfExactType<ActionIconTheme>();
return actionIconTheme?.data ?? Theme.of(context).actionIconTheme;
}
@override
Widget wrap(BuildContext context, Widget child) {
return ActionIconTheme(data: data, child: child);
}
@override
bool updateShouldNotify(ActionIconTheme oldWidget) => data != oldWidget.data;
}

View file

@ -9,8 +9,8 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'action_buttons.dart';
import 'app_bar_theme.dart';
import 'back_button.dart';
import 'button_style.dart';
import 'color_scheme.dart';
import 'colors.dart';
@ -21,7 +21,6 @@ import 'icon_button.dart';
import 'icon_button_theme.dart';
import 'icons.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'material_state.dart';
import 'scaffold.dart';
import 'tabs.dart';
@ -755,14 +754,6 @@ class _AppBarState extends State<AppBar> {
super.dispose();
}
void _handleDrawerButton() {
Scaffold.of(context).openDrawer();
}
void _handleDrawerButtonEnd() {
Scaffold.of(context).openEndDrawer();
}
void _handleScrollNotification(ScrollNotification notification) {
if (notification is ScrollUpdateNotification && widget.notificationPredicate(notification)) {
final bool oldScrolledUnder = _scrolledUnder;
@ -894,11 +885,8 @@ class _AppBarState extends State<AppBar> {
Widget? leading = widget.leading;
if (leading == null && widget.automaticallyImplyLeading) {
if (hasDrawer) {
leading = IconButton(
icon: const Icon(Icons.menu),
iconSize: overallIconTheme.size ?? 24,
onPressed: _handleDrawerButton,
tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
leading = DrawerButton(
style: IconButton.styleFrom(iconSize: overallIconTheme.size ?? 24),
);
// TODO(chunhtai): remove (!hasEndDrawer && canPop) once internal tests
// are migrated.
@ -1009,11 +997,8 @@ class _AppBarState extends State<AppBar> {
children: widget.actions!,
);
} else if (hasEndDrawer) {
actions = IconButton(
icon: const Icon(Icons.menu),
iconSize: overallIconTheme.size ?? 24,
onPressed: _handleDrawerButtonEnd,
tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
actions = EndDrawerButton(
style: IconButton.styleFrom(iconSize: overallIconTheme.size ?? 24),
);
}

View file

@ -2,197 +2,4 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'debug.dart';
import 'icon_button.dart';
import 'icons.dart';
import 'material_localizations.dart';
import 'theme.dart';
/// A "back" icon that's appropriate for the current [TargetPlatform].
///
/// The current platform is determined by querying for the ambient [Theme].
///
/// See also:
///
/// * [BackButton], an [IconButton] with a [BackButtonIcon] that calls
/// [Navigator.maybePop] to return to the previous route.
/// * [IconButton], which is a more general widget for creating buttons
/// with icons.
/// * [Icon], a Material Design icon.
/// * [ThemeData.platform], which specifies the current platform.
class BackButtonIcon extends StatelessWidget {
/// Creates an icon that shows the appropriate "back" image for
/// the current platform (as obtained from the [Theme]).
const BackButtonIcon({ super.key });
@override
Widget build(BuildContext context) {
final String? semanticsLabel;
final IconData data;
switch (Theme.of(context).platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
data = Icons.arrow_back;
break;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
data = Icons.arrow_back_ios;
break;
}
// This can't use the platform from Theme because it is the Android OS that
// expects the duplicated tooltip and label.
switch (defaultTargetPlatform) {
case TargetPlatform.android:
semanticsLabel = MaterialLocalizations.of(context).backButtonTooltip;
break;
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
case TargetPlatform.iOS:
case TargetPlatform.macOS:
semanticsLabel = null;
break;
}
return Icon(data, semanticLabel: semanticsLabel);
}
}
/// A Material Design back button.
///
/// A [BackButton] is an [IconButton] with a "back" icon appropriate for the
/// current [TargetPlatform]. When pressed, the back button calls
/// [Navigator.maybePop] to return to the previous route unless a custom
/// [onPressed] callback is provided.
///
/// When deciding to display a [BackButton], consider using
/// `ModalRoute.of(context)?.canPop` to check whether the current route can be
/// popped. If that value is false (e.g., because the current route is the
/// initial route), the [BackButton] will not have any effect when pressed,
/// which could frustrate the user.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
///
/// * [AppBar], which automatically uses a [BackButton] in its
/// [AppBar.leading] slot when the [Scaffold] has no [Drawer] and the
/// current [Route] is not the [Navigator]'s first route.
/// * [BackButtonIcon], which is useful if you need to create a back button
/// that responds differently to being pressed.
/// * [IconButton], which is a more general widget for creating buttons with
/// icons.
/// * [CloseButton], an alternative which may be more appropriate for leaf
/// node pages in the navigation tree.
class BackButton extends StatelessWidget {
/// Creates an [IconButton] with the appropriate "back" icon for the current
/// target platform.
const BackButton({ super.key, this.color, this.onPressed });
/// The color to use for the icon.
///
/// Defaults to the [IconThemeData.color] specified in the ambient [IconTheme],
/// which usually matches the ambient [Theme]'s [ThemeData.iconTheme].
final Color? color;
/// An override callback to perform instead of the default behavior which is
/// to pop the [Navigator].
///
/// It can, for instance, be used to pop the platform's navigation stack
/// via [SystemNavigator] instead of Flutter's [Navigator] in add-to-app
/// situations.
///
/// Defaults to null.
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
return IconButton(
icon: const BackButtonIcon(),
color: color,
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
onPressed: () {
if (onPressed != null) {
onPressed!();
} else {
Navigator.maybePop(context);
}
},
);
}
}
/// A Material Design close button.
///
/// A [CloseButton] is an [IconButton] with a "close" icon. When pressed, the
/// close button calls [Navigator.maybePop] to return to the previous route.
///
/// Use a [CloseButton] instead of a [BackButton] on fullscreen dialogs or
/// pages that may solicit additional actions to close.
///
/// See also:
///
/// * [AppBar], which automatically uses a [CloseButton] in its
/// [AppBar.leading] slot when appropriate.
/// * [BackButton], which is more appropriate for middle nodes in the
/// navigation tree or where pages can be popped instantaneously with
/// no user data consequence.
/// * [IconButton], to create other Material Design icon buttons.
class CloseButton extends StatelessWidget {
/// Creates a Material Design close button.
const CloseButton({ super.key, this.color, this.onPressed });
/// The color to use for the icon.
///
/// Defaults to the [IconThemeData.color] specified in the ambient [IconTheme],
/// which usually matches the ambient [Theme]'s [ThemeData.iconTheme].
final Color? color;
/// An override callback to perform instead of the default behavior which is
/// to pop the [Navigator].
///
/// It can, for instance, be used to pop the platform's navigation stack
/// via [SystemNavigator] instead of Flutter's [Navigator] in add-to-app
/// situations.
///
/// Defaults to null.
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
final String? semanticsLabel;
// This can't use the platform from Theme because it is the Android OS that
// expects the duplicated tooltip and label.
switch (defaultTargetPlatform) {
case TargetPlatform.android:
semanticsLabel = MaterialLocalizations.of(context).closeButtonTooltip;
break;
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
case TargetPlatform.iOS:
case TargetPlatform.macOS:
semanticsLabel = null;
break;
}
return IconButton(
icon: Icon(Icons.close, semanticLabel: semanticsLabel),
color: color,
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
onPressed: () {
if (onPressed != null) {
onPressed!();
} else {
Navigator.maybePop(context);
}
},
);
}
}
export 'action_buttons.dart' show BackButton, BackButtonIcon, CloseButton, CloseButtonIcon;

View file

@ -7,6 +7,8 @@ import 'dart:ui' show Color, lerpDouble;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'action_buttons.dart';
import 'action_icons_theme.dart';
import 'app_bar_theme.dart';
import 'badge_theme.dart';
import 'banner_theme.dart';
@ -337,6 +339,7 @@ class ThemeData with Diagnosticable {
TextTheme? textTheme,
Typography? typography,
// COMPONENT THEMES
ActionIconThemeData? actionIconTheme,
AppBarTheme? appBarTheme,
BadgeThemeData? badgeTheme,
MaterialBannerThemeData? bannerTheme,
@ -649,6 +652,7 @@ class ThemeData with Diagnosticable {
typography: typography,
primaryIconTheme: primaryIconTheme,
// COMPONENT THEMES
actionIconTheme: actionIconTheme,
appBarTheme: appBarTheme,
badgeTheme: badgeTheme,
bannerTheme: bannerTheme,
@ -759,6 +763,7 @@ class ThemeData with Diagnosticable {
required this.textTheme,
required this.typography,
// COMPONENT THEMES
required this.actionIconTheme,
required this.appBarTheme,
required this.badgeTheme,
required this.bannerTheme,
@ -1356,6 +1361,10 @@ class ThemeData with Diagnosticable {
// COMPONENT THEMES
/// A theme for customizing icons of [BackButtonIcon], [CloseButtonIcon],
/// [DrawerButtonIcon], or [EndDrawerButtonIcon].
final ActionIconThemeData? actionIconTheme;
/// A theme for customizing the color, elevation, brightness, iconTheme and
/// textTheme of [AppBar]s.
final AppBarTheme appBarTheme;
@ -1654,6 +1663,7 @@ class ThemeData with Diagnosticable {
TextTheme? textTheme,
Typography? typography,
// COMPONENT THEMES
ActionIconThemeData? actionIconTheme,
AppBarTheme? appBarTheme,
BadgeThemeData? badgeTheme,
MaterialBannerThemeData? bannerTheme,
@ -1787,6 +1797,7 @@ class ThemeData with Diagnosticable {
textTheme: textTheme ?? this.textTheme,
typography: typography ?? this.typography,
// COMPONENT THEMES
actionIconTheme: actionIconTheme ?? this.actionIconTheme,
appBarTheme: appBarTheme ?? this.appBarTheme,
badgeTheme: badgeTheme ?? this.badgeTheme,
bannerTheme: bannerTheme ?? this.bannerTheme,
@ -1980,6 +1991,7 @@ class ThemeData with Diagnosticable {
textTheme: TextTheme.lerp(a.textTheme, b.textTheme, t),
typography: Typography.lerp(a.typography, b.typography, t),
// COMPONENT THEMES
actionIconTheme: ActionIconThemeData.lerp(a.actionIconTheme, b.actionIconTheme, t),
appBarTheme: AppBarTheme.lerp(a.appBarTheme, b.appBarTheme, t),
badgeTheme: BadgeThemeData.lerp(a.badgeTheme, b.badgeTheme, t),
bannerTheme: MaterialBannerThemeData.lerp(a.bannerTheme, b.bannerTheme, t),
@ -2085,6 +2097,7 @@ class ThemeData with Diagnosticable {
other.textTheme == textTheme &&
other.typography == typography &&
// COMPONENT THEMES
other.actionIconTheme == actionIconTheme &&
other.appBarTheme == appBarTheme &&
other.badgeTheme == badgeTheme &&
other.bannerTheme == bannerTheme &&
@ -2187,6 +2200,7 @@ class ThemeData with Diagnosticable {
textTheme,
typography,
// COMPONENT THEMES
actionIconTheme,
appBarTheme,
badgeTheme,
bannerTheme,
@ -2291,6 +2305,7 @@ class ThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<TextTheme>('textTheme', textTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<Typography>('typography', typography, defaultValue: defaultData.typography, level: DiagnosticLevel.debug));
// COMPONENT THEMES
properties.add(DiagnosticsProperty<ActionIconThemeData>('actionIconTheme', actionIconTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<AppBarTheme>('appBarTheme', appBarTheme, defaultValue: defaultData.appBarTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<BadgeThemeData>('badgeTheme', badgeTheme, defaultValue: defaultData.badgeTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<MaterialBannerThemeData>('bannerTheme', bannerTheme, defaultValue: defaultData.bannerTheme, level: DiagnosticLevel.debug));

View file

@ -0,0 +1,183 @@
// 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/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('ActionIconThemeData copyWith, ==, hashCode basics', () {
expect(const ActionIconThemeData(), const ActionIconThemeData().copyWith());
expect(const ActionIconThemeData().hashCode,
const ActionIconThemeData().copyWith().hashCode);
});
test('ActionIconThemeData defaults', () {
const ActionIconThemeData themeData = ActionIconThemeData();
expect(themeData.backButtonIconBuilder, null);
expect(themeData.closeButtonIconBuilder, null);
expect(themeData.drawerButtonIconBuilder, null);
expect(themeData.endDrawerButtonIconBuilder, null);
});
testWidgets('Default ActionIconThemeData debugFillProperties',
(WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const ActionIconThemeData().debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[]);
});
testWidgets('ActionIconThemeData implements debugFillProperties',
(WidgetTester tester) async {
Widget actionButtonIconBuilder(BuildContext context) {
return const Icon(IconData(0));
}
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
ActionIconThemeData(
backButtonIconBuilder: actionButtonIconBuilder,
closeButtonIconBuilder: actionButtonIconBuilder,
drawerButtonIconBuilder: actionButtonIconBuilder,
endDrawerButtonIconBuilder: actionButtonIconBuilder,
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
final Matcher containsBuilderCallback = contains('Closure: (BuildContext) =>');
expect(description, <dynamic>[
allOf(startsWith('backButtonIconBuilder:'), containsBuilderCallback),
allOf(startsWith('closeButtonIconBuilder:'), containsBuilderCallback),
allOf(startsWith('drawerButtonIconBuilder:'), containsBuilderCallback),
allOf(startsWith('endDrawerButtonIconBuilder:'), containsBuilderCallback),
]);
});
testWidgets('Action buttons use ThemeData action icon theme', (WidgetTester tester) async {
const Color green = Color(0xff00ff00);
const IconData icon = IconData(0);
Widget buildSampleIcon(BuildContext context) {
return const Icon(
icon,
size: 20,
color: green,
);
}
final ActionIconThemeData actionIconTheme = ActionIconThemeData(
backButtonIconBuilder: buildSampleIcon,
closeButtonIconBuilder: buildSampleIcon,
drawerButtonIconBuilder: buildSampleIcon,
endDrawerButtonIconBuilder: buildSampleIcon,
);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.light(useMaterial3: true).copyWith(
actionIconTheme: actionIconTheme,
),
home: const Material(
child: Column(
children: <Widget>[
BackButton(),
CloseButton(),
DrawerButton(),
EndDrawerButton(),
],
),
),
),
);
final Icon backButtonIcon = tester.widget(find.descendant(of: find.byType(BackButton), matching: find.byType(Icon)));
final Icon closeButtonIcon = tester.widget(find.descendant(of: find.byType(CloseButton), matching: find.byType(Icon)));
final Icon drawerButtonIcon = tester.widget(find.descendant(of: find.byType(DrawerButton), matching: find.byType(Icon)));
final Icon endDrawerButtonIcon = tester.widget(find.descendant(of: find.byType(EndDrawerButton), matching: find.byType(Icon)));
expect(backButtonIcon.icon == icon, isTrue);
expect(closeButtonIcon.icon == icon, isTrue);
expect(drawerButtonIcon.icon == icon, isTrue);
expect(endDrawerButtonIcon.icon == icon, isTrue);
final RichText backButtonIconText = tester.widget(find.descendant(of: find.byType(BackButton), matching: find.byType(RichText)));
final RichText closeButtonIconText = tester.widget(find.descendant(of: find.byType(CloseButton), matching: find.byType(RichText)));
final RichText drawerButtonIconText = tester.widget(find.descendant(of: find.byType(DrawerButton), matching: find.byType(RichText)));
final RichText endDrawerButtonIconText = tester.widget(find.descendant(of: find.byType(EndDrawerButton), matching: find.byType(RichText)));
expect(backButtonIconText.text.style!.color, green);
expect(closeButtonIconText.text.style!.color, green);
expect(drawerButtonIconText.text.style!.color, green);
expect(endDrawerButtonIconText.text.style!.color, green);
});
// This test is essentially the same as 'Action buttons use ThemeData action icon theme'. In
// this case the theme is introduced with the ActionIconTheme widget instead of
// ThemeData.actionIconTheme.
testWidgets('Action buttons use ActionIconTheme', (WidgetTester tester) async {
const Color green = Color(0xff00ff00);
const IconData icon = IconData(0);
Widget buildSampleIcon(BuildContext context) {
return const Icon(
icon,
size: 20,
color: green,
);
}
final ActionIconThemeData actionIconTheme = ActionIconThemeData(
backButtonIconBuilder: buildSampleIcon,
closeButtonIconBuilder: buildSampleIcon,
drawerButtonIconBuilder: buildSampleIcon,
endDrawerButtonIconBuilder: buildSampleIcon,
);
await tester.pumpWidget(
MaterialApp(
home: ActionIconTheme(
data: actionIconTheme,
child: const Material(
child: Column(
children: <Widget>[
BackButton(),
CloseButton(),
DrawerButton(),
EndDrawerButton(),
],
),
),
),
),
);
final Icon backButtonIcon = tester.widget(find.descendant(of: find.byType(BackButton), matching: find.byType(Icon)));
final Icon closeButtonIcon = tester.widget(find.descendant(of: find.byType(CloseButton), matching: find.byType(Icon)));
final Icon drawerButtonIcon = tester.widget(find.descendant(of: find.byType(DrawerButton), matching: find.byType(Icon)));
final Icon endDrawerButtonIcon = tester.widget(find.descendant(of: find.byType(EndDrawerButton), matching: find.byType(Icon)));
expect(backButtonIcon.icon == icon, isTrue);
expect(closeButtonIcon.icon == icon, isTrue);
expect(drawerButtonIcon.icon == icon, isTrue);
expect(endDrawerButtonIcon.icon == icon, isTrue);
final RichText backButtonIconText = tester.widget(find.descendant(of: find.byType(BackButton), matching: find.byType(RichText)));
final RichText closeButtonIconText = tester.widget(find.descendant(of: find.byType(CloseButton), matching: find.byType(RichText)));
final RichText drawerButtonIconText = tester.widget(find.descendant(of: find.byType(DrawerButton), matching: find.byType(RichText)));
final RichText endDrawerButtonIconText = tester.widget(find.descendant(of: find.byType(EndDrawerButton), matching: find.byType(RichText)));
expect(backButtonIconText.text.style!.color, green);
expect(closeButtonIconText.text.style!.color, green);
expect(drawerButtonIconText.text.style!.color, green);
expect(endDrawerButtonIconText.text.style!.color, green);
});
}

View file

@ -108,9 +108,9 @@ void main() {
final Icon linuxIcon = tester.widget(find.descendant(of: find.byKey(linuxKey), matching: find.byType(Icon)));
final Icon macOSIcon = tester.widget(find.descendant(of: find.byKey(macOSKey), matching: find.byType(Icon)));
final Icon windowsIcon = tester.widget(find.descendant(of: find.byKey(windowsKey), matching: find.byType(Icon)));
expect(iOSIcon.icon == androidIcon.icon, isFalse);
expect(iOSIcon.icon == androidIcon.icon, kIsWeb ? isTrue : isFalse);
expect(linuxIcon.icon == androidIcon.icon, isTrue);
expect(macOSIcon.icon == androidIcon.icon, isFalse);
expect(macOSIcon.icon == androidIcon.icon, kIsWeb ? isTrue : isFalse);
expect(macOSIcon.icon == iOSIcon.icon, isTrue);
expect(windowsIcon.icon == androidIcon.icon, isTrue);
});
@ -120,7 +120,7 @@ void main() {
const MaterialApp(
home: Material(
child: BackButton(
color: Colors.blue,
color: Colors.red,
),
),
),
@ -130,7 +130,51 @@ void main() {
of: find.byType(BackButton),
matching: find.byType(RichText),
));
expect(iconText.text.style!.color, Colors.blue);
expect(iconText.text.style!.color, Colors.red);
});
testWidgets('BackButton color with ButtonStyle', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: true),
home: const Material(
child: BackButton(
style: ButtonStyle(
iconColor: MaterialStatePropertyAll<Color>(Colors.red),
),
),
),
),
);
final RichText iconText = tester.firstWidget(find.descendant(
of: find.byType(BackButton),
matching: find.byType(RichText),
));
expect(iconText.text.style!.color, Colors.red);
});
testWidgets('BackButton.style.iconColor parameter overrides BackButton.color', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: true),
home: const Material(
child: BackButton(
color: Colors.green,
style: ButtonStyle(
iconColor: MaterialStatePropertyAll<Color>(Colors.red),
),
),
),
),
);
final RichText iconText = tester.firstWidget(find.descendant(
of: find.byType(BackButton),
matching: find.byType(RichText),
));
expect(iconText.text.style!.color, Colors.red);
});
testWidgets('BackButton semantics', (WidgetTester tester) async {
@ -239,6 +283,50 @@ void main() {
expect(iconText.text.style!.color, Colors.red);
});
testWidgets('CloseButton color with ButtonStyle', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: true),
home: const Material(
child: CloseButton(
style: ButtonStyle(
iconColor: MaterialStatePropertyAll<Color>(Colors.red),
),
),
),
),
);
final RichText iconText = tester.firstWidget(find.descendant(
of: find.byType(CloseButton),
matching: find.byType(RichText),
));
expect(iconText.text.style!.color, Colors.red);
});
testWidgets('CloseButton.style.iconColor parameter overrides CloseButton.color', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: true),
home: const Material(
child: CloseButton(
color: Colors.green,
style: ButtonStyle(
iconColor: MaterialStatePropertyAll<Color>(Colors.red),
),
),
),
),
);
final RichText iconText = tester.firstWidget(find.descendant(
of: find.byType(CloseButton),
matching: find.byType(RichText),
));
expect(iconText.text.style!.color, Colors.red);
});
testWidgets('CloseButton onPressed overrides default pop behavior', (WidgetTester tester) async {
bool customCallbackWasCalled = false;
await tester.pumpWidget(

View file

@ -0,0 +1,279 @@
// 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/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('DrawerButton control test', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: DrawerButton(),
drawer: Drawer(),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(Drawer), findsNothing);
await tester.tap(find.byType(DrawerButton));
await tester.pumpAndSettle();
expect(find.byType(Drawer), findsOneWidget);
});
testWidgets('DrawerButton onPressed overrides default end drawer open behaviour',
(WidgetTester tester) async {
bool customCallbackWasCalled = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: DrawerButton(
onPressed: () => customCallbackWasCalled = true),
),
drawer: const Drawer(),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(Drawer), findsNothing); // Start off with a closed drawer
expect(customCallbackWasCalled,
false); // customCallbackWasCalled should still be false.
await tester.tap(find.byType(DrawerButton));
await tester.pumpAndSettle();
// Drawer is still closed
expect(find.byType(Drawer), findsNothing);
// The custom callback is called, setting customCallbackWasCalled to true.
expect(customCallbackWasCalled, true);
});
testWidgets('DrawerButton icon', (WidgetTester tester) async {
final Key androidKey = UniqueKey();
final Key iOSKey = UniqueKey();
final Key linuxKey = UniqueKey();
final Key macOSKey = UniqueKey();
final Key windowsKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Column(
children: <Widget>[
Theme(
data: ThemeData(platform: TargetPlatform.android),
child: DrawerButtonIcon(key: androidKey),
),
Theme(
data: ThemeData(platform: TargetPlatform.iOS),
child: DrawerButtonIcon(key: iOSKey),
),
Theme(
data: ThemeData(platform: TargetPlatform.linux),
child: DrawerButtonIcon(key: linuxKey),
),
Theme(
data: ThemeData(platform: TargetPlatform.macOS),
child: DrawerButtonIcon(key: macOSKey),
),
Theme(
data: ThemeData(platform: TargetPlatform.windows),
child: DrawerButtonIcon(key: windowsKey),
),
],
),
),
);
final Icon androidIcon = tester.widget(find.descendant(
of: find.byKey(androidKey), matching: find.byType(Icon)));
final Icon iOSIcon = tester.widget(
find.descendant(of: find.byKey(iOSKey), matching: find.byType(Icon)));
final Icon linuxIcon = tester.widget(
find.descendant(of: find.byKey(linuxKey), matching: find.byType(Icon)));
final Icon macOSIcon = tester.widget(
find.descendant(of: find.byKey(macOSKey), matching: find.byType(Icon)));
final Icon windowsIcon = tester.widget(find.descendant(
of: find.byKey(windowsKey), matching: find.byType(Icon)));
// All icons for drawer are the same
expect(iOSIcon.icon == androidIcon.icon, isTrue);
expect(linuxIcon.icon == androidIcon.icon, isTrue);
expect(macOSIcon.icon == androidIcon.icon, isTrue);
expect(windowsIcon.icon == androidIcon.icon, isTrue);
});
testWidgets('DrawerButton color', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: true),
home: const Material(
child: DrawerButton(
style: ButtonStyle(
iconColor: MaterialStatePropertyAll<Color>(Colors.red),
),
),
),
),
);
final RichText iconText = tester.firstWidget(find.descendant(
of: find.byType(DrawerButton),
matching: find.byType(RichText),
));
expect(iconText.text.style!.color, Colors.red);
});
testWidgets('DrawerButton semantics', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Center(
child: DrawerButton(),
),
),
),
);
await tester.pumpAndSettle();
final String? expectedLabel;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
expectedLabel = 'Open navigation menu';
break;
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
expectedLabel = null;
}
expect(tester.getSemantics(find.byType(DrawerButton)), matchesSemantics(
tooltip: 'Open navigation menu',
label: expectedLabel,
isButton: true,
hasEnabledState: true,
isEnabled: true,
hasTapAction: true,
isFocusable: true,
));
handle.dispose();
}, variant: TargetPlatformVariant.all());
testWidgets('EndDrawerButton control test', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: EndDrawerButton(),
endDrawer: Drawer(),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(Drawer), findsNothing);
await tester.tap(find.byType(EndDrawerButton));
await tester.pumpAndSettle();
expect(find.byType(Drawer), findsOneWidget);
});
testWidgets('EndDrawerButton semantics', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Center(
child: EndDrawerButton(),
),
),
),
);
await tester.pumpAndSettle();
final String? expectedLabel;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
expectedLabel = 'Open navigation menu';
break;
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
expectedLabel = null;
}
expect(tester.getSemantics(find.byType(EndDrawerButton)), matchesSemantics(
tooltip: 'Open navigation menu',
label: expectedLabel,
isButton: true,
hasEnabledState: true,
isEnabled: true,
hasTapAction: true,
isFocusable: true,
));
handle.dispose();
}, variant: TargetPlatformVariant.all());
testWidgets('EndDrawerButton color', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: true),
home: const Material(
child: EndDrawerButton(
style: ButtonStyle(
iconColor: MaterialStatePropertyAll<Color>(Colors.red),
),
),
),
),
);
final RichText iconText = tester.firstWidget(find.descendant(
of: find.byType(EndDrawerButton),
matching: find.byType(RichText),
));
expect(iconText.text.style!.color, Colors.red);
});
testWidgets('EndDrawerButton onPressed overrides default end drawer open behaviour',
(WidgetTester tester) async {
bool customCallbackWasCalled = false;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: EndDrawerButton(onPressed: () => customCallbackWasCalled = true),
),
endDrawer: const Drawer(),
),
),
);
await tester.pumpAndSettle();
expect(find.byType(Drawer), findsNothing); // Start off with a closed drawer
expect(customCallbackWasCalled,
false); // customCallbackWasCalled should still be false.
await tester.tap(find.byType(EndDrawerButton));
await tester.pumpAndSettle();
// Drawer is still closed
expect(find.byType(Drawer), findsNothing);
// The custom callback is called, setting customCallbackWasCalled to true.
expect(customCallbackWasCalled, true);
});
}

View file

@ -769,7 +769,7 @@ void main() {
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }));
testWidgets('Back arrow uses correct default', (WidgetTester tester) async {
await expectBackIcon(tester, Icons.arrow_back_ios);
await expectBackIcon(tester, kIsWeb ? Icons.arrow_back : Icons.arrow_back_ios);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
});

View file

@ -757,6 +757,7 @@ void main() {
textTheme: ThemeData.dark().textTheme,
typography: Typography.material2018(),
// COMPONENT THEMES
actionIconTheme: const ActionIconThemeData(),
appBarTheme: const AppBarTheme(backgroundColor: Colors.black),
badgeTheme: const BadgeThemeData(backgroundColor: Colors.black),
bannerTheme: const MaterialBannerThemeData(backgroundColor: Colors.black),
@ -874,6 +875,7 @@ void main() {
typography: Typography.material2018(platform: TargetPlatform.iOS),
// COMPONENT THEMES
actionIconTheme: const ActionIconThemeData(),
appBarTheme: const AppBarTheme(backgroundColor: Colors.white),
badgeTheme: const BadgeThemeData(backgroundColor: Colors.black),
bannerTheme: const MaterialBannerThemeData(backgroundColor: Colors.white),
@ -977,6 +979,7 @@ void main() {
typography: otherTheme.typography,
// COMPONENT THEMES
actionIconTheme: otherTheme.actionIconTheme,
appBarTheme: otherTheme.appBarTheme,
badgeTheme: otherTheme.badgeTheme,
bannerTheme: otherTheme.bannerTheme,
@ -1077,6 +1080,7 @@ void main() {
expect(themeDataCopy.typography, equals(otherTheme.typography));
// COMPONENT THEMES
expect(themeDataCopy.actionIconTheme, equals(otherTheme.actionIconTheme));
expect(themeDataCopy.appBarTheme, equals(otherTheme.appBarTheme));
expect(themeDataCopy.badgeTheme, equals(otherTheme.badgeTheme));
expect(themeDataCopy.bannerTheme, equals(otherTheme.bannerTheme));
@ -1211,6 +1215,7 @@ void main() {
'iconTheme',
'primaryIconTheme',
// COMPONENT THEMES
'actionIconTheme',
'appBarTheme',
'badgeTheme',
'bannerTheme',