Unverified Commit 66a84d1f authored by Qun Cheng's avatar Qun Cheng Committed by GitHub

Migrate `IconButton` to Material 3 - Part 1 (#105176)

* Added standard IconButton for M3 with new ButtonStyle field

* Added IconButton examples for standard, filled, filled_tonal, and outlined types
Co-authored-by: 's avatarQun Cheng <quncheng@google.com>
parent 17185191
...@@ -22,6 +22,7 @@ import 'package:gen_defaults/button_template.dart'; ...@@ -22,6 +22,7 @@ import 'package:gen_defaults/button_template.dart';
import 'package:gen_defaults/card_template.dart'; import 'package:gen_defaults/card_template.dart';
import 'package:gen_defaults/dialog_template.dart'; import 'package:gen_defaults/dialog_template.dart';
import 'package:gen_defaults/fab_template.dart'; import 'package:gen_defaults/fab_template.dart';
import 'package:gen_defaults/icon_button_template.dart';
import 'package:gen_defaults/navigation_bar_template.dart'; import 'package:gen_defaults/navigation_bar_template.dart';
import 'package:gen_defaults/navigation_rail_template.dart'; import 'package:gen_defaults/navigation_rail_template.dart';
import 'package:gen_defaults/surface_tint.dart'; import 'package:gen_defaults/surface_tint.dart';
...@@ -55,6 +56,9 @@ Future<void> main(List<String> args) async { ...@@ -55,6 +56,9 @@ Future<void> main(List<String> args) async {
'fab_large_primary.json', 'fab_large_primary.json',
'fab_primary.json', 'fab_primary.json',
'fab_small_primary.json', 'fab_small_primary.json',
'icon_button.json',
'icon_button_filled.json',
'icon_button_filled_tonal.json',
'motion.json', 'motion.json',
'navigation_bar.json', 'navigation_bar.json',
'navigation_rail.json', 'navigation_rail.json',
...@@ -86,6 +90,7 @@ Future<void> main(List<String> args) async { ...@@ -86,6 +90,7 @@ Future<void> main(List<String> args) async {
CardTemplate('$materialLib/card.dart', tokens).updateFile(); CardTemplate('$materialLib/card.dart', tokens).updateFile();
DialogTemplate('$materialLib/dialog.dart', tokens).updateFile(); DialogTemplate('$materialLib/dialog.dart', tokens).updateFile();
FABTemplate('$materialLib/floating_action_button.dart', tokens).updateFile(); FABTemplate('$materialLib/floating_action_button.dart', tokens).updateFile();
IconButtonTemplate('$materialLib/icon_button.dart', tokens).updateFile();
NavigationBarTemplate('$materialLib/navigation_bar.dart', tokens).updateFile(); NavigationBarTemplate('$materialLib/navigation_bar.dart', tokens).updateFile();
NavigationRailTemplate('$materialLib/navigation_rail.dart', tokens).updateFile(); NavigationRailTemplate('$materialLib/navigation_rail.dart', tokens).updateFile();
SurfaceTintTemplate('$materialLib/elevation_overlay.dart', tokens).updateFile(); SurfaceTintTemplate('$materialLib/elevation_overlay.dart', tokens).updateFile();
......
// 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 IconButtonTemplate extends TokenTemplate {
const IconButtonTemplate(super.fileName, super.tokens)
: super(colorSchemePrefix: '_colors.',
);
@override
String generate() => '''
// Generated version ${tokens["version"]}
class _TokenDefaultsM3 extends ButtonStyle {
_TokenDefaultsM3(this.context)
: super(
animationDuration: kThemeChangeDuration,
enableFeedback: true,
alignment: Alignment.center,
);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
// No default text style
@override
MaterialStateProperty<Color?>? get backgroundColor =>
ButtonStyleButton.allOrNull<Color>(Colors.transparent);
@override
MaterialStateProperty<Color?>? get foregroundColor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return ${componentColor('md.comp.icon-button.disabled.icon')};
return ${componentColor('md.comp.icon-button.unselected.icon')};
});
@override
MaterialStateProperty<Color?>? get overlayColor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered))
return ${componentColor('md.comp.icon-button.unselected.hover.state-layer')};
if (states.contains(MaterialState.focused))
return ${componentColor('md.comp.icon-button.unselected.focus.state-layer')};
if (states.contains(MaterialState.pressed))
return ${componentColor('md.comp.icon-button.unselected.pressed.state-layer')};
return null;
});
// No default shadow color
// No default surface tint color
@override
MaterialStateProperty<double>? get elevation =>
ButtonStyleButton.allOrNull<double>(0.0);
@override
MaterialStateProperty<EdgeInsetsGeometry>? get padding =>
ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(const EdgeInsets.all(8.0));
@override
MaterialStateProperty<Size>? get minimumSize =>
ButtonStyleButton.allOrNull<Size>(const Size(${tokens["md.comp.icon-button.state-layer.size"]}, ${tokens["md.comp.icon-button.state-layer.size"]}));
// No default fixedSize
@override
MaterialStateProperty<Size>? get maximumSize =>
ButtonStyleButton.allOrNull<Size>(Size.infinite);
// No default side
@override
MaterialStateProperty<OutlinedBorder>? get shape =>
ButtonStyleButton.allOrNull<OutlinedBorder>(${shape("md.comp.icon-button.state-layer")});
@override
MaterialStateProperty<MouseCursor?>? get mouseCursor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return SystemMouseCursors.basic;
return SystemMouseCursors.click;
});
@override
VisualDensity? get visualDensity => Theme.of(context).visualDensity;
@override
MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
}
''';
}
// 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 IconButton
import 'package:flutter/material.dart';
void main() {
runApp(const IconButtonApp());
}
class IconButtonApp extends StatelessWidget {
const IconButtonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true),
title: 'Icon Button Types',
home: const Scaffold(
body: ButtonTypesExample(),
),
);
}
}
class ButtonTypesExample extends StatelessWidget {
const ButtonTypesExample({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
children: const <Widget>[
Spacer(),
ButtonTypesGroup(enabled: true),
ButtonTypesGroup(enabled: false),
Spacer(),
],
),
);
}
}
class ButtonTypesGroup extends StatelessWidget {
const ButtonTypesGroup({ super.key, required this.enabled });
final bool enabled;
@override
Widget build(BuildContext context) {
final VoidCallback? onPressed = enabled ? () {} : null;
final ColorScheme colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.all(4.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
IconButton(icon: const Icon(Icons.filter_drama), onPressed: onPressed),
// Use a standard IconButton with specific style to implement the
// 'Filled' type.
IconButton(
icon: const Icon(Icons.filter_drama),
onPressed: onPressed,
style: IconButton.styleFrom(
foregroundColor: colors.onPrimary,
backgroundColor: colors.primary,
disabledBackgroundColor: colors.onSurface.withOpacity(0.12),
hoverColor: colors.onPrimary.withOpacity(0.08),
focusColor: colors.onPrimary.withOpacity(0.12),
highlightColor: colors.onPrimary.withOpacity(0.12),
)
),
// Use a standard IconButton with specific style to implement the
// 'Filled Tonal' type.
IconButton(
icon: const Icon(Icons.filter_drama),
onPressed: onPressed,
style: IconButton.styleFrom(
foregroundColor: colors.onSecondaryContainer,
backgroundColor: colors.secondaryContainer,
disabledBackgroundColor: colors.onSurface.withOpacity(0.12),
hoverColor: colors.onSecondaryContainer.withOpacity(0.08),
focusColor: colors.onSecondaryContainer.withOpacity(0.12),
highlightColor: colors.onSecondaryContainer.withOpacity(0.12),
),
),
// Use a standard IconButton with specific style to implement the
// 'Outlined' type.
IconButton(
icon: const Icon(Icons.filter_drama),
onPressed: onPressed,
style: IconButton.styleFrom(
focusColor: colors.onSurfaceVariant.withOpacity(0.12),
highlightColor: colors.onSurface.withOpacity(0.12),
side: onPressed == null
? BorderSide(color: Theme.of(context).colorScheme.onSurface.withOpacity(0.12))
: BorderSide(color: colors.outline),
).copyWith(
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return colors.onSurface;
}
return null;
}),
),
),
],
),
);
}
}
...@@ -8,11 +8,16 @@ import 'package:flutter/foundation.dart'; ...@@ -8,11 +8,16 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'button_style.dart';
import 'button_style_button.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart'; import 'constants.dart';
import 'debug.dart'; import 'debug.dart';
import 'icons.dart'; import 'icons.dart';
import 'ink_well.dart'; import 'ink_well.dart';
import 'material.dart'; import 'material.dart';
import 'material_state.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart'; import 'theme_data.dart';
import 'tooltip.dart'; import 'tooltip.dart';
...@@ -92,6 +97,17 @@ const double _kMinButtonSize = kMinInteractiveDimension; ...@@ -92,6 +97,17 @@ const double _kMinButtonSize = kMinInteractiveDimension;
/// ** See code in examples/api/lib/material/icon_button/icon_button.1.dart ** /// ** See code in examples/api/lib/material/icon_button/icon_button.1.dart **
/// {@end-tool} /// {@end-tool}
/// ///
/// Material Design 3 introduced new types (standard and contained) of [IconButton]s.
/// The default [IconButton] is the standard type, and contained icon buttons can be produced
/// by configuring the [IconButton] widget's properties.
///
/// {@tool dartpad}
/// This sample shows creation of [IconButton] widgets for standard, filled,
/// filled tonal and outlined types, as described in: https://m3.material.io/components/icon-buttons/overview
///
/// ** See code in examples/api/lib/material/icon_button/icon_button.2.dart **
/// {@end-tool}
///
/// See also: /// See also:
/// ///
/// * [Icons], the library of Material Icons. /// * [Icons], the library of Material Icons.
...@@ -134,6 +150,7 @@ class IconButton extends StatelessWidget { ...@@ -134,6 +150,7 @@ class IconButton extends StatelessWidget {
this.tooltip, this.tooltip,
this.enableFeedback = true, this.enableFeedback = true,
this.constraints, this.constraints,
this.style,
required this.icon, required this.icon,
}) : assert(padding != null), }) : assert(padding != null),
assert(alignment != null), assert(alignment != null),
...@@ -184,6 +201,8 @@ class IconButton extends StatelessWidget { ...@@ -184,6 +201,8 @@ class IconButton extends StatelessWidget {
/// The splash radius. /// The splash radius.
/// ///
/// If [ThemeData.useMaterial3] is set to true, this will not be used.
///
/// If null, default splash radius of [Material.defaultSplashRadius] is used. /// If null, default splash radius of [Material.defaultSplashRadius] is used.
final double? splashRadius; final double? splashRadius;
...@@ -230,6 +249,8 @@ class IconButton extends StatelessWidget { ...@@ -230,6 +249,8 @@ class IconButton extends StatelessWidget {
/// fill the button area if the touch is held for long enough time. If the splash /// fill the button area if the touch is held for long enough time. If the splash
/// color has transparency then the highlight and button color will show through. /// color has transparency then the highlight and button color will show through.
/// ///
/// If [ThemeData.useMaterial3] is set to true, this will not be used.
///
/// Defaults to the Theme's splash color, [ThemeData.splashColor]. /// Defaults to the Theme's splash color, [ThemeData.splashColor].
final Color? splashColor; final Color? splashColor;
...@@ -301,10 +322,125 @@ class IconButton extends StatelessWidget { ...@@ -301,10 +322,125 @@ class IconButton extends StatelessWidget {
/// and `Theme.of(context).visualDensity` otherwise. /// and `Theme.of(context).visualDensity` otherwise.
final BoxConstraints? constraints; final BoxConstraints? constraints;
/// Customizes this button's appearance.
///
/// Non-null properties of this style override the corresponding
/// properties in [_IconButtonM3.themeStyleOf] and [_IconButtonM3.defaultStyleOf].
/// [MaterialStateProperty]s that resolve to non-null values will similarly
/// override the corresponding [MaterialStateProperty]s in [_IconButtonM3.themeStyleOf]
/// and [_IconButtonM3.defaultStyleOf].
///
/// The [style] is only used for Material 3 [IconButton]. 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].
///
/// For example, if [IconButton]'s [visualDensity] is set to [VisualDensity.standard]
/// and [style]'s [visualDensity] is set to [VisualDensity.compact],
/// the icon button will have [VisualDensity.compact] to define the button's layout.
///
/// Null by default.
final ButtonStyle? style;
/// A static convenience method that constructs an icon button
/// [ButtonStyle] given simple values. This method is only used for Material 3.
///
/// The [foregroundColor] color is used to create a [MaterialStateProperty]
/// [ButtonStyle.foregroundColor] value. Specify a value for [foregroundColor]
/// to specify the color of the button's icons. The [hoverColor], [focusColor]
/// and [highlightColor] colors are used to indicate the hover, focus,
/// and pressed states. Use [backgroundColor] for the button's background
/// fill color. Use [disabledForegroundColor] and [disabledBackgroundColor]
/// to specify the button's disabled icon and fill color.
///
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
/// parameters are used to construct [ButtonStyle].mouseCursor.
///
/// All of the other parameters are either used directly or used to
/// create a [MaterialStateProperty] with a single value for all
/// states.
///
/// All parameters default to null, by default this method returns
/// a [ButtonStyle] that doesn't override anything.
///
/// For example, to override the default icon color for a
/// [IconButton], as well as its overlay color, with all of the
/// standard opacity adjustments for the pressed, focused, and
/// hovered states, one could write:
///
/// ```dart
/// IconButton(
/// style: IconButton.styleFrom(foregroundColor: Colors.green),
/// )
/// ```
static ButtonStyle styleFrom({
Color? foregroundColor,
Color? backgroundColor,
Color? disabledForegroundColor,
Color? disabledBackgroundColor,
Color? focusColor,
Color? hoverColor,
Color? highlightColor,
Color? shadowColor,
Color? surfaceTintColor,
double? elevation,
Size? minimumSize,
Size? fixedSize,
Size? maximumSize,
BorderSide? side,
OutlinedBorder? shape,
EdgeInsetsGeometry? padding,
MouseCursor? enabledMouseCursor,
MouseCursor? disabledMouseCursor,
VisualDensity? visualDensity,
MaterialTapTargetSize? tapTargetSize,
Duration? animationDuration,
bool? enableFeedback,
AlignmentGeometry? alignment,
InteractiveInkFeatureFactory? splashFactory,
}) {
final MaterialStateProperty<Color?>? buttonBackgroundColor = (backgroundColor == null && disabledBackgroundColor == null)
? null
: _IconButtonDefaultBackground(backgroundColor, disabledBackgroundColor);
final MaterialStateProperty<Color?>? buttonForegroundColor = (foregroundColor == null && disabledForegroundColor == null)
? null
: _IconButtonDefaultForeground(foregroundColor, disabledForegroundColor);
final MaterialStateProperty<Color?>? overlayColor = (foregroundColor == null && hoverColor == null && focusColor == null && highlightColor == null)
? null
: _IconButtonDefaultOverlay(foregroundColor, focusColor, hoverColor, highlightColor);
final MaterialStateProperty<MouseCursor>? mouseCursor = (enabledMouseCursor == null && disabledMouseCursor == null)
? null
: _IconButtonDefaultMouseCursor(enabledMouseCursor!, disabledMouseCursor!);
return ButtonStyle(
backgroundColor: buttonBackgroundColor,
foregroundColor: buttonForegroundColor,
overlayColor: overlayColor,
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
elevation: ButtonStyleButton.allOrNull<double>(elevation),
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize),
maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
side: ButtonStyleButton.allOrNull<BorderSide>(side),
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
mouseCursor: mouseCursor,
visualDensity: visualDensity,
tapTargetSize: tapTargetSize,
animationDuration: animationDuration,
enableFeedback: enableFeedback,
alignment: alignment,
splashFactory: splashFactory,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
if (!theme.useMaterial3) {
assert(debugCheckHasMaterial(context));
}
Color? currentColor; Color? currentColor;
if (onPressed != null) { if (onPressed != null) {
currentColor = color; currentColor = color;
...@@ -321,6 +457,55 @@ class IconButton extends StatelessWidget { ...@@ -321,6 +457,55 @@ class IconButton extends StatelessWidget {
final BoxConstraints adjustedConstraints = effectiveVisualDensity.effectiveConstraints(unadjustedConstraints); final BoxConstraints adjustedConstraints = effectiveVisualDensity.effectiveConstraints(unadjustedConstraints);
final double effectiveIconSize = iconSize ?? IconTheme.of(context).size ?? 24.0; final double effectiveIconSize = iconSize ?? IconTheme.of(context).size ?? 24.0;
if (theme.useMaterial3) {
final Size? minSize = constraints == null
? null
: Size(constraints!.minWidth, constraints!.minHeight);
final Size? maxSize = constraints == null
? null
: Size(constraints!.maxWidth, constraints!.maxHeight);
ButtonStyle adjustedStyle = styleFrom(
visualDensity: visualDensity,
foregroundColor: color,
disabledForegroundColor: disabledColor,
focusColor: focusColor,
hoverColor: hoverColor,
highlightColor: highlightColor,
padding: padding,
minimumSize: minSize,
maximumSize: maxSize,
alignment: alignment,
enabledMouseCursor: mouseCursor,
disabledMouseCursor: mouseCursor,
enableFeedback: enableFeedback,
);
if (style != null) {
adjustedStyle = style!.merge(adjustedStyle);
}
Widget iconButton = IconTheme.merge(
data: IconThemeData(
size: effectiveIconSize,
),
child: icon,
);
if (tooltip != null) {
iconButton = Tooltip(
message: tooltip,
child: iconButton,
);
}
return _IconButtonM3(
key: key,
style: adjustedStyle,
onPressed: onPressed,
autofocus: autofocus,
focusNode: focusNode,
child: iconButton,
);
}
Widget result = ConstrainedBox( Widget result = ConstrainedBox(
constraints: adjustedConstraints, constraints: adjustedConstraints,
child: Padding( child: Padding(
...@@ -389,3 +574,245 @@ class IconButton extends StatelessWidget { ...@@ -389,3 +574,245 @@ class IconButton extends StatelessWidget {
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null)); properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
} }
} }
class _IconButtonM3 extends ButtonStyleButton {
const _IconButtonM3({
super.key,
required super.onPressed,
super.style,
super.focusNode,
super.autofocus = false,
required Widget super.child,
}) : super(
onLongPress: null,
onHover: null,
onFocusChange: null,
clipBehavior: Clip.none);
/// ## Material 3 defaults
///
/// If [ThemeData.useMaterial3] is set to true the following defaults will
/// be used:
///
/// * `textStyle` - null
/// * `backgroundColor` - transparent
/// * `foregroundColor`
/// * disabled - Theme.colorScheme.onSurface(0.38)
/// * others - Theme.colorScheme.onSurfaceVariant
/// * `overlayColor`
/// * hovered or focused - Theme.colorScheme.onSurfaceVariant(0.08)
/// * pressed - Theme.colorScheme.onSurfaceVariant(0.12)
/// * others - null
/// * `shadowColor` - null
/// * `surfaceTintColor` - null
/// * `elevation` - 0
/// * `padding` - all(8)
/// * `minimumSize` - Size(40, 40)
/// * `fixedSize` - null
/// * `maximumSize` - Size.infinite
/// * `side` - null
/// * `shape` - StadiumBorder()
/// * `mouseCursor`
/// * disabled - SystemMouseCursors.basic
/// * others - SystemMouseCursors.click
/// * `visualDensity` - theme.visualDensity
/// * `tapTargetSize` - theme.materialTapTargetSize
/// * `animationDuration` - kThemeChangeDuration
/// * `enableFeedback` - true
/// * `alignment` - Alignment.center
/// * `splashFactory` - Theme.splashFactory
@override
ButtonStyle defaultStyleOf(BuildContext context) {
return _TokenDefaultsM3(context);
}
/// Returns null because [IconButton] doesn't have its component theme.
@override
ButtonStyle? themeStyleOf(BuildContext context) {
return null;
}
}
@immutable
class _IconButtonDefaultBackground extends MaterialStateProperty<Color?> {
_IconButtonDefaultBackground(this.background, this.disabledBackground);
final Color? background;
final Color? disabledBackground;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledBackground;
}
return background;
}
@override
String toString() {
return '{disabled: $disabledBackground, otherwise: $background}';
}
}
@immutable
class _IconButtonDefaultForeground extends MaterialStateProperty<Color?> {
_IconButtonDefaultForeground(this.foregroundColor, this.disabledForegroundColor);
final Color? foregroundColor;
final Color? disabledForegroundColor;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledForegroundColor;
}
return foregroundColor;
}
@override
String toString() {
return '{disabled: $disabledForegroundColor, otherwise: $foregroundColor}';
}
}
@immutable
class _IconButtonDefaultOverlay extends MaterialStateProperty<Color?> {
_IconButtonDefaultOverlay(this.foregroundColor, this.focusColor, this.hoverColor, this.highlightColor);
final Color? foregroundColor;
final Color? focusColor;
final Color? hoverColor;
final Color? highlightColor;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return hoverColor ?? foregroundColor?.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return focusColor ?? foregroundColor?.withOpacity(0.08);
}
if (states.contains(MaterialState.pressed)) {
return highlightColor ?? foregroundColor?.withOpacity(0.12);
}
return null;
}
@override
String toString() {
return '{hovered: $hoverColor, focused: $focusColor, pressed: $highlightColor, otherwise: null}';
}
}
@immutable
class _IconButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor> with Diagnosticable {
_IconButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
final MouseCursor enabledCursor;
final MouseCursor disabledCursor;
@override
MouseCursor resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledCursor;
}
return enabledCursor;
}
}
// BEGIN GENERATED TOKEN PROPERTIES
// Generated code to the end of this file. Do not edit by hand.
// These defaults are generated from the Material Design Token
// database by the script dev/tools/gen_defaults/bin/gen_defaults.dart.
// Generated version v0_98
class _TokenDefaultsM3 extends ButtonStyle {
_TokenDefaultsM3(this.context)
: super(
animationDuration: kThemeChangeDuration,
enableFeedback: true,
alignment: Alignment.center,
);
final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;
// No default text style
@override
MaterialStateProperty<Color?>? get backgroundColor =>
ButtonStyleButton.allOrNull<Color>(Colors.transparent);
@override
MaterialStateProperty<Color?>? get foregroundColor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return _colors.onSurface.withOpacity(0.38);
}
return _colors.onSurfaceVariant;
});
@override
MaterialStateProperty<Color?>? get overlayColor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return _colors.onSurfaceVariant.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return _colors.onSurfaceVariant.withOpacity(0.08);
}
if (states.contains(MaterialState.pressed)) {
return _colors.onSurfaceVariant.withOpacity(0.12);
}
return null;
});
// No default shadow color
// No default surface tint color
@override
MaterialStateProperty<double>? get elevation =>
ButtonStyleButton.allOrNull<double>(0.0);
@override
MaterialStateProperty<EdgeInsetsGeometry>? get padding =>
ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(const EdgeInsets.all(8.0));
@override
MaterialStateProperty<Size>? get minimumSize =>
ButtonStyleButton.allOrNull<Size>(const Size(40.0, 40.0));
// No default fixedSize
@override
MaterialStateProperty<Size>? get maximumSize =>
ButtonStyleButton.allOrNull<Size>(Size.infinite);
// No default side
@override
MaterialStateProperty<OutlinedBorder>? get shape =>
ButtonStyleButton.allOrNull<OutlinedBorder>(const StadiumBorder());
@override
MaterialStateProperty<MouseCursor?>? get mouseCursor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return SystemMouseCursors.basic;
}
return SystemMouseCursors.click;
});
@override
VisualDensity? get visualDensity => Theme.of(context).visualDensity;
@override
MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;
@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
}
// END GENERATED TOKEN PROPERTIES
...@@ -22,14 +22,17 @@ class MockOnPressedFunction { ...@@ -22,14 +22,17 @@ class MockOnPressedFunction {
void main() { void main() {
late MockOnPressedFunction mockOnPressedFunction; late MockOnPressedFunction mockOnPressedFunction;
const ColorScheme colorScheme = ColorScheme.light();
final ThemeData theme = ThemeData.from(colorScheme: colorScheme);
setUp(() { setUp(() {
mockOnPressedFunction = MockOnPressedFunction(); mockOnPressedFunction = MockOnPressedFunction();
}); });
testWidgets('test default icon buttons are sized up to 48', (WidgetTester tester) async { testWidgets('test default icon buttons are sized up to 48', (WidgetTester tester) async {
final bool material3 = theme.useMaterial3;
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: material3,
child: IconButton( child: IconButton(
onPressed: mockOnPressedFunction.handler, onPressed: mockOnPressedFunction.handler,
icon: const Icon(Icons.link), icon: const Icon(Icons.link),
...@@ -45,14 +48,16 @@ void main() { ...@@ -45,14 +48,16 @@ void main() {
}); });
testWidgets('test small icons are sized up to 48dp', (WidgetTester tester) async { testWidgets('test small icons are sized up to 48dp', (WidgetTester tester) async {
final bool material3 = theme.useMaterial3;
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: material3,
child: IconButton( child: IconButton(
iconSize: 10.0, iconSize: 10.0,
onPressed: mockOnPressedFunction.handler, onPressed: mockOnPressedFunction.handler,
icon: const Icon(Icons.link), icon: const Icon(Icons.link),
), ),
), )
); );
final RenderBox iconButton = tester.renderObject(find.byType(IconButton)); final RenderBox iconButton = tester.renderObject(find.byType(IconButton));
...@@ -60,15 +65,17 @@ void main() { ...@@ -60,15 +65,17 @@ void main() {
}); });
testWidgets('test icons can be small when total size is >48dp', (WidgetTester tester) async { testWidgets('test icons can be small when total size is >48dp', (WidgetTester tester) async {
final bool material3 = theme.useMaterial3;
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: material3,
child: IconButton( child: IconButton(
iconSize: 10.0, iconSize: 10.0,
padding: const EdgeInsets.all(30.0), padding: const EdgeInsets.all(30.0),
onPressed: mockOnPressedFunction.handler, onPressed: mockOnPressedFunction.handler,
icon: const Icon(Icons.link), icon: const Icon(Icons.link),
), ),
), )
); );
final RenderBox iconButton = tester.renderObject(find.byType(IconButton)); final RenderBox iconButton = tester.renderObject(find.byType(IconButton));
...@@ -76,18 +83,19 @@ void main() { ...@@ -76,18 +83,19 @@ void main() {
}); });
testWidgets('when both iconSize and IconTheme.of(context).size are null, size falls back to 24.0', (WidgetTester tester) async { testWidgets('when both iconSize and IconTheme.of(context).size are null, size falls back to 24.0', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); final bool material3 = theme.useMaterial3;
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: material3,
child: IconTheme( child: IconTheme(
data: const IconThemeData(), data: const IconThemeData(),
child: IconButton( child: IconButton(
focusNode: focusNode, focusNode: FocusNode(debugLabel: 'Ink Focus'),
onPressed: mockOnPressedFunction.handler, onPressed: mockOnPressedFunction.handler,
icon: const Icon(Icons.link), icon: const Icon(Icons.link),
), ),
) )
), )
); );
final RenderBox icon = tester.renderObject(find.byType(Icon)); final RenderBox icon = tester.renderObject(find.byType(Icon));
...@@ -96,9 +104,11 @@ void main() { ...@@ -96,9 +104,11 @@ void main() {
testWidgets('when null, iconSize is overridden by closest IconTheme', (WidgetTester tester) async { testWidgets('when null, iconSize is overridden by closest IconTheme', (WidgetTester tester) async {
RenderBox icon; RenderBox icon;
final bool material3 = theme.useMaterial3;
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: material3,
child: IconTheme( child: IconTheme(
data: const IconThemeData(size: 10), data: const IconThemeData(size: 10),
child: IconButton( child: IconButton(
...@@ -106,7 +116,7 @@ void main() { ...@@ -106,7 +116,7 @@ void main() {
icon: const Icon(Icons.link), icon: const Icon(Icons.link),
), ),
) )
), )
); );
icon = tester.renderObject(find.byType(Icon)); icon = tester.renderObject(find.byType(Icon));
...@@ -114,8 +124,10 @@ void main() { ...@@ -114,8 +124,10 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: material3,
child: Theme( child: Theme(
data: ThemeData( data: ThemeData(
useMaterial3: material3,
iconTheme: const IconThemeData(size: 10), iconTheme: const IconThemeData(size: 10),
), ),
child: IconButton( child: IconButton(
...@@ -123,7 +135,7 @@ void main() { ...@@ -123,7 +135,7 @@ void main() {
icon: const Icon(Icons.link), icon: const Icon(Icons.link),
), ),
) )
), )
); );
icon = tester.renderObject(find.byType(Icon)); icon = tester.renderObject(find.byType(Icon));
...@@ -131,8 +143,10 @@ void main() { ...@@ -131,8 +143,10 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: material3,
child: Theme( child: Theme(
data: ThemeData( data: ThemeData(
useMaterial3: material3,
iconTheme: const IconThemeData(size: 20), iconTheme: const IconThemeData(size: 20),
), ),
child: IconTheme( child: IconTheme(
...@@ -151,10 +165,12 @@ void main() { ...@@ -151,10 +165,12 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: material3,
child: IconTheme( child: IconTheme(
data: const IconThemeData(size: 20), data: const IconThemeData(size: 20),
child: Theme( child: Theme(
data: ThemeData( data: ThemeData(
useMaterial3: material3,
iconTheme: const IconThemeData(size: 10), iconTheme: const IconThemeData(size: 10),
), ),
child: IconButton( child: IconButton(
...@@ -171,8 +187,10 @@ void main() { ...@@ -171,8 +187,10 @@ void main() {
}); });
testWidgets('when non-null, iconSize precedes IconTheme.of(context).size', (WidgetTester tester) async { testWidgets('when non-null, iconSize precedes IconTheme.of(context).size', (WidgetTester tester) async {
final bool material3 = theme.useMaterial3;
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: material3,
child: IconTheme( child: IconTheme(
data: const IconThemeData(size: 30.0), data: const IconThemeData(size: 30.0),
child: IconButton( child: IconButton(
...@@ -188,28 +206,36 @@ void main() { ...@@ -188,28 +206,36 @@ void main() {
expect(icon.size, const Size(10.0, 10.0)); expect(icon.size, const Size(10.0, 10.0));
}); });
testWidgets('Small icons with non-null constraints can be <48dp', (WidgetTester tester) async { testWidgets('Small icons with non-null constraints can be <48dp for M2, but =48dp for M3', (WidgetTester tester) async {
final bool material3 = theme.useMaterial3;
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: material3,
child: IconButton( child: IconButton(
iconSize: 10.0, iconSize: 10.0,
onPressed: mockOnPressedFunction.handler, onPressed: mockOnPressedFunction.handler,
icon: const Icon(Icons.link), icon: const Icon(Icons.link),
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
), )
), )
); );
final RenderBox iconButton = tester.renderObject(find.byType(IconButton)); final RenderBox iconButton = tester.renderObject(find.byType(IconButton));
final RenderBox icon = tester.renderObject(find.byType(Icon));
// By default IconButton has a padding of 8.0 on all sides, so both // By default IconButton has a padding of 8.0 on all sides, so both
// width and height are 10.0 + 2 * 8.0 = 26.0 // width and height are 10.0 + 2 * 8.0 = 26.0
expect(iconButton.size, const Size(26.0, 26.0)); // M3 IconButton is a subclass of ButtonStyleButton which has a minimum
// Size(48.0, 48.0).
expect(iconButton.size, material3 ? const Size(48.0, 48.0) : const Size(26.0, 26.0));
expect(icon.size, const Size(10.0, 10.0));
}); });
testWidgets('Small icons with non-null constraints and custom padding can be <48dp', (WidgetTester tester) async { testWidgets('Small icons with non-null constraints and custom padding can be <48dp', (WidgetTester tester) async {
final bool material3 = theme.useMaterial3;
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: material3,
child: IconButton( child: IconButton(
iconSize: 10.0, iconSize: 10.0,
padding: const EdgeInsets.all(3.0), padding: const EdgeInsets.all(3.0),
...@@ -221,17 +247,23 @@ void main() { ...@@ -221,17 +247,23 @@ void main() {
); );
final RenderBox iconButton = tester.renderObject(find.byType(IconButton)); final RenderBox iconButton = tester.renderObject(find.byType(IconButton));
final RenderBox icon = tester.renderObject(find.byType(Icon));
// This IconButton has a padding of 3.0 on all sides, so both // This IconButton has a padding of 3.0 on all sides, so both
// width and height are 10.0 + 2 * 3.0 = 16.0 // width and height are 10.0 + 2 * 3.0 = 16.0
expect(iconButton.size, const Size(16.0, 16.0)); // M3 IconButton is a subclass of ButtonStyleButton which has a minimum
// Size(48.0, 48.0).
expect(iconButton.size, material3 ? const Size(48.0, 48.0) : const Size(16.0, 16.0));
expect(icon.size, const Size(10.0, 10.0));
}); });
testWidgets('Small icons comply with VisualDensity requirements', (WidgetTester tester) async { testWidgets('Small icons comply with VisualDensity requirements', (WidgetTester tester) async {
final bool material3 = theme.useMaterial3;
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: material3,
child: Theme( child: Theme(
data: ThemeData(visualDensity: const VisualDensity(horizontal: 1, vertical: -1)), data: ThemeData(visualDensity: const VisualDensity(horizontal: 1, vertical: -1), useMaterial3: material3),
child: IconButton( child: IconButton(
iconSize: 10.0, iconSize: 10.0,
onPressed: mockOnPressedFunction.handler, onPressed: mockOnPressedFunction.handler,
...@@ -248,12 +280,13 @@ void main() { ...@@ -248,12 +280,13 @@ void main() {
// width by 4 pixels and decreases its height by 4 pixels, giving // width by 4 pixels and decreases its height by 4 pixels, giving
// final width 32.0 + 4.0 = 36.0 and // final width 32.0 + 4.0 = 36.0 and
// final height 32.0 - 4.0 = 28.0 // final height 32.0 - 4.0 = 28.0
expect(iconButton.size, const Size(36.0, 28.0)); expect(iconButton.size, material3 ? const Size(52.0, 44.0) : const Size(36.0, 28.0));
}); });
testWidgets('test default icon buttons are constrained', (WidgetTester tester) async { testWidgets('test default icon buttons are constrained', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: theme.useMaterial3,
child: IconButton( child: IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
onPressed: mockOnPressedFunction.handler, onPressed: mockOnPressedFunction.handler,
...@@ -287,11 +320,34 @@ void main() { ...@@ -287,11 +320,34 @@ void main() {
final RenderBox box = tester.renderObject(find.byType(IconButton)); final RenderBox box = tester.renderObject(find.byType(IconButton));
expect(box.size, const Size(48.0, 600.0)); expect(box.size, const Size(48.0, 600.0));
// Test for Material 3
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: colorScheme, useMaterial3: true),
home: Directionality(
textDirection: TextDirection.ltr,
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget> [
IconButton(
onPressed: mockOnPressedFunction.handler,
icon: const Icon(Icons.ac_unit),
),
],
),
),
)
);
final RenderBox boxM3 = tester.renderObject(find.byType(IconButton));
expect(boxM3.size, const Size(48.0, 600.0));
}); });
testWidgets('test default padding', (WidgetTester tester) async { testWidgets('test default padding', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: theme.useMaterial3,
child: IconButton( child: IconButton(
onPressed: mockOnPressedFunction.handler, onPressed: mockOnPressedFunction.handler,
icon: const Icon(Icons.ac_unit), icon: const Icon(Icons.ac_unit),
...@@ -307,6 +363,7 @@ void main() { ...@@ -307,6 +363,7 @@ void main() {
testWidgets('test tooltip', (WidgetTester tester) async { testWidgets('test tooltip', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: theme,
home: Material( home: Material(
child: Center( child: Center(
child: IconButton( child: IconButton(
...@@ -325,6 +382,7 @@ void main() { ...@@ -325,6 +382,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: theme,
home: Material( home: Material(
child: Center( child: Center(
child: IconButton( child: IconButton(
...@@ -347,6 +405,7 @@ void main() { ...@@ -347,6 +405,7 @@ void main() {
testWidgets('IconButton AppBar size', (WidgetTester tester) async { testWidgets('IconButton AppBar size', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: theme,
home: Scaffold( home: Scaffold(
appBar: AppBar( appBar: AppBar(
actions: <Widget>[ actions: <Widget>[
...@@ -368,11 +427,12 @@ void main() { ...@@ -368,11 +427,12 @@ void main() {
// This test is very similar to the '...explicit splashColor and highlightColor' test // This test is very similar to the '...explicit splashColor and highlightColor' test
// in buttons_test.dart. If you change this one, you may want to also change that one. // in buttons_test.dart. If you change this one, you may want to also change that one.
testWidgets('IconButton with explicit splashColor and highlightColor', (WidgetTester tester) async { testWidgets('IconButton with explicit splashColor and highlightColor - M2', (WidgetTester tester) async {
const Color directSplashColor = Color(0xFF00000F); const Color directSplashColor = Color(0xFF00000F);
const Color directHighlightColor = Color(0xFF0000F0); const Color directHighlightColor = Color(0xFF0000F0);
Widget buttonWidget = wrap( Widget buttonWidget = wrap(
useMaterial3: false,
child: IconButton( child: IconButton(
icon: const Icon(Icons.android), icon: const Icon(Icons.android),
splashColor: directSplashColor, splashColor: directSplashColor,
...@@ -383,7 +443,7 @@ void main() { ...@@ -383,7 +443,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
Theme( Theme(
data: ThemeData(), data: ThemeData(useMaterial3: false),
child: buttonWidget, child: buttonWidget,
), ),
); );
...@@ -404,6 +464,7 @@ void main() { ...@@ -404,6 +464,7 @@ void main() {
const Color themeHighlightColor1 = Color(0xFF00FF00); const Color themeHighlightColor1 = Color(0xFF00FF00);
buttonWidget = wrap( buttonWidget = wrap(
useMaterial3: false,
child: IconButton( child: IconButton(
icon: const Icon(Icons.android), icon: const Icon(Icons.android),
onPressed: () { /* enable the button */ }, onPressed: () { /* enable the button */ },
...@@ -415,6 +476,7 @@ void main() { ...@@ -415,6 +476,7 @@ void main() {
data: ThemeData( data: ThemeData(
highlightColor: themeHighlightColor1, highlightColor: themeHighlightColor1,
splashColor: themeSplashColor1, splashColor: themeSplashColor1,
useMaterial3: false,
), ),
child: buttonWidget, child: buttonWidget,
), ),
...@@ -435,6 +497,7 @@ void main() { ...@@ -435,6 +497,7 @@ void main() {
data: ThemeData( data: ThemeData(
highlightColor: themeHighlightColor2, highlightColor: themeHighlightColor2,
splashColor: themeSplashColor2, splashColor: themeSplashColor2,
useMaterial3: false,
), ),
child: buttonWidget, // same widget, so does not get updated because of us child: buttonWidget, // same widget, so does not get updated because of us
), ),
...@@ -450,10 +513,11 @@ void main() { ...@@ -450,10 +513,11 @@ void main() {
await gesture.up(); await gesture.up();
}); });
testWidgets('IconButton with explicit splash radius', (WidgetTester tester) async { testWidgets('IconButton with explicit splash radius - M2', (WidgetTester tester) async {
const double splashRadius = 30.0; const double splashRadius = 30.0;
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Material( home: Material(
child: Center( child: Center(
child: IconButton( child: IconButton(
...@@ -480,11 +544,12 @@ void main() { ...@@ -480,11 +544,12 @@ void main() {
await gesture.up(); await gesture.up();
}); });
testWidgets('IconButton Semantics (enabled)', (WidgetTester tester) async { testWidgets('IconButton Semantics (enabled) - M2', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: false,
child: IconButton( child: IconButton(
onPressed: mockOnPressedFunction.handler, onPressed: mockOnPressedFunction.handler,
icon: const Icon(Icons.link, semanticLabel: 'link'), icon: const Icon(Icons.link, semanticLabel: 'link'),
...@@ -513,11 +578,12 @@ void main() { ...@@ -513,11 +578,12 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('IconButton Semantics (disabled)', (WidgetTester tester) async { testWidgets('IconButton Semantics (disabled) - M2', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: false,
child: const IconButton( child: const IconButton(
onPressed: null, onPressed: null,
icon: Icon(Icons.link, semanticLabel: 'link'), icon: Icon(Icons.link, semanticLabel: 'link'),
...@@ -545,6 +611,7 @@ void main() { ...@@ -545,6 +611,7 @@ void main() {
final FocusNode focusNode = FocusNode(debugLabel: 'IconButton'); final FocusNode focusNode = FocusNode(debugLabel: 'IconButton');
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: theme.useMaterial3,
child: IconButton( child: IconButton(
focusNode: focusNode, focusNode: focusNode,
autofocus: true, autofocus: true,
...@@ -559,6 +626,7 @@ void main() { ...@@ -559,6 +626,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: theme.useMaterial3,
child: IconButton( child: IconButton(
focusNode: focusNode, focusNode: focusNode,
autofocus: true, autofocus: true,
...@@ -575,6 +643,7 @@ void main() { ...@@ -575,6 +643,7 @@ void main() {
final FocusNode focusNode = FocusNode(debugLabel: 'IconButton'); final FocusNode focusNode = FocusNode(debugLabel: 'IconButton');
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: theme.useMaterial3,
child: MediaQuery( child: MediaQuery(
data: const MediaQueryData( data: const MediaQueryData(
navigationMode: NavigationMode.directional, navigationMode: NavigationMode.directional,
...@@ -594,6 +663,7 @@ void main() { ...@@ -594,6 +663,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: theme.useMaterial3,
child: MediaQuery( child: MediaQuery(
data: const MediaQueryData( data: const MediaQueryData(
navigationMode: NavigationMode.directional, navigationMode: NavigationMode.directional,
...@@ -617,6 +687,7 @@ void main() { ...@@ -617,6 +687,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
useMaterial3: theme.useMaterial3,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
IconButton( IconButton(
...@@ -658,8 +729,7 @@ void main() { ...@@ -658,8 +729,7 @@ void main() {
}); });
testWidgets('IconButton with disabled feedback', (WidgetTester tester) async { testWidgets('IconButton with disabled feedback', (WidgetTester tester) async {
await tester.pumpWidget(Material( final Widget button = Directionality(
child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Center( child: Center(
child: IconButton( child: IconButton(
...@@ -668,8 +738,13 @@ void main() { ...@@ -668,8 +738,13 @@ void main() {
icon: const Icon(Icons.link), icon: const Icon(Icons.link),
), ),
), ),
), );
));
await tester.pumpWidget(
theme.useMaterial3
? MaterialApp(theme: theme, home: button)
: Material(child: button)
);
await tester.tap(find.byType(IconButton), pointer: 1); await tester.tap(find.byType(IconButton), pointer: 1);
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 0); expect(feedback.clickSoundCount, 0);
...@@ -677,8 +752,7 @@ void main() { ...@@ -677,8 +752,7 @@ void main() {
}); });
testWidgets('IconButton with enabled feedback', (WidgetTester tester) async { testWidgets('IconButton with enabled feedback', (WidgetTester tester) async {
await tester.pumpWidget(Material( final Widget button = Directionality(
child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Center( child: Center(
child: IconButton( child: IconButton(
...@@ -686,8 +760,13 @@ void main() { ...@@ -686,8 +760,13 @@ void main() {
icon: const Icon(Icons.link), icon: const Icon(Icons.link),
), ),
), ),
), );
));
await tester.pumpWidget(
theme.useMaterial3
? MaterialApp(theme: theme, home: button)
: Material(child: button),
);
await tester.tap(find.byType(IconButton), pointer: 1); await tester.tap(find.byType(IconButton), pointer: 1);
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 1); expect(feedback.clickSoundCount, 1);
...@@ -695,8 +774,7 @@ void main() { ...@@ -695,8 +774,7 @@ void main() {
}); });
testWidgets('IconButton with enabled feedback by default', (WidgetTester tester) async { testWidgets('IconButton with enabled feedback by default', (WidgetTester tester) async {
await tester.pumpWidget(Material( final Widget button = Directionality(
child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Center( child: Center(
child: IconButton( child: IconButton(
...@@ -704,8 +782,13 @@ void main() { ...@@ -704,8 +782,13 @@ void main() {
icon: const Icon(Icons.link), icon: const Icon(Icons.link),
), ),
), ),
), );
));
await tester.pumpWidget(
theme.useMaterial3
? MaterialApp(theme: theme, home: button)
: Material(child: button),
);
await tester.tap(find.byType(IconButton), pointer: 1); await tester.tap(find.byType(IconButton), pointer: 1);
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 1); expect(feedback.clickSoundCount, 1);
...@@ -715,9 +798,11 @@ void main() { ...@@ -715,9 +798,11 @@ void main() {
testWidgets('IconButton responds to density changes.', (WidgetTester tester) async { testWidgets('IconButton responds to density changes.', (WidgetTester tester) async {
const Key key = Key('test'); const Key key = Key('test');
final bool material3 = theme.useMaterial3;
Future<void> buildTest(VisualDensity visualDensity) async { Future<void> buildTest(VisualDensity visualDensity) async {
return tester.pumpWidget( return tester.pumpWidget(
MaterialApp( MaterialApp(
theme: theme,
home: Material( home: Material(
child: Center( child: Center(
child: IconButton( child: IconButton(
...@@ -733,27 +818,34 @@ void main() { ...@@ -733,27 +818,34 @@ void main() {
} }
await buildTest(VisualDensity.standard); await buildTest(VisualDensity.standard);
final RenderBox box = tester.renderObject(find.byKey(key)); final RenderBox box = tester.renderObject(find.byType(IconButton));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(box.size, equals(const Size(48, 48))); expect(box.size, equals(const Size(48, 48)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0)); await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(box.size, equals(const Size(60, 60))); expect(box.size, equals(material3 ? const Size(64, 64) : const Size(60, 60)));
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0)); await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(box.size, equals(const Size(40, 40))); // IconButton is a subclass of ButtonStyleButton in Material 3, so the negative
// visualDensity cannot be applied to horizontal padding.
// The size of the Button with padding is (24 + 8 + 8, 24) -> (40, 24)
// minSize of M3 IconButton is (48 - 12, 48 - 12) -> (36, 36)
// So, the button size in Material 3 is (40, 36)
expect(box.size, equals(material3 ? const Size(40, 36) : const Size(40, 40)));
await buildTest(const VisualDensity(horizontal: 3.0, vertical: -3.0)); await buildTest(const VisualDensity(horizontal: 3.0, vertical: -3.0));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(box.size, equals(const Size(60, 40))); expect(box.size, equals(material3 ? const Size(64, 36) : const Size(60, 40)));
}); });
testWidgets('IconButton.mouseCursor changes cursor on hover', (WidgetTester tester) async { testWidgets('IconButton.mouseCursor changes cursor on hover', (WidgetTester tester) async {
// Test argument works // Test argument works
await tester.pumpWidget( await tester.pumpWidget(
Material( MaterialApp(
theme: theme,
home: Material(
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Center( child: Center(
...@@ -765,6 +857,7 @@ void main() { ...@@ -765,6 +857,7 @@ void main() {
), ),
), ),
), ),
),
); );
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
...@@ -776,7 +869,9 @@ void main() { ...@@ -776,7 +869,9 @@ void main() {
// Test default is click // Test default is click
await tester.pumpWidget( await tester.pumpWidget(
Material( MaterialApp(
theme: theme,
home: Material(
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Center( child: Center(
...@@ -787,6 +882,7 @@ void main() { ...@@ -787,6 +882,7 @@ void main() {
), ),
), ),
), ),
),
); );
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
...@@ -794,7 +890,9 @@ void main() { ...@@ -794,7 +890,9 @@ void main() {
testWidgets('disabled IconButton has basic mouse cursor', (WidgetTester tester) async { testWidgets('disabled IconButton has basic mouse cursor', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const Material( MaterialApp(
theme: theme,
home: const Material(
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Center( child: Center(
...@@ -805,6 +903,7 @@ void main() { ...@@ -805,6 +903,7 @@ void main() {
), ),
), ),
), ),
),
); );
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
...@@ -817,7 +916,9 @@ void main() { ...@@ -817,7 +916,9 @@ void main() {
testWidgets('IconButton.mouseCursor overrides implicit setting of mouse cursor', (WidgetTester tester) async { testWidgets('IconButton.mouseCursor overrides implicit setting of mouse cursor', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const Material( MaterialApp(
theme: theme,
home: const Material(
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Center( child: Center(
...@@ -829,6 +930,7 @@ void main() { ...@@ -829,6 +930,7 @@ void main() {
), ),
), ),
), ),
),
); );
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
...@@ -839,7 +941,9 @@ void main() { ...@@ -839,7 +941,9 @@ void main() {
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.none); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.none);
await tester.pumpWidget( await tester.pumpWidget(
Material( MaterialApp(
theme: theme,
home: Material(
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Center( child: Center(
...@@ -851,14 +955,368 @@ void main() { ...@@ -851,14 +955,368 @@ void main() {
), ),
), ),
), ),
),
); );
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.none); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.none);
}); });
testWidgets('IconButton defaults - M3', (WidgetTester tester) async {
final ThemeData themeM3 = ThemeData.from(colorScheme: colorScheme, useMaterial3: true);
// Enabled IconButton
await tester.pumpWidget(
MaterialApp(
theme: themeM3,
home: Center(
child: IconButton(
onPressed: () { },
icon: const Icon(Icons.ac_unit),
),
),
),
);
final Finder buttonMaterial = find.descendant(
of: find.byType(IconButton),
matching: find.byType(Material),
);
Material material = tester.widget<Material>(buttonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, Colors.transparent);
expect(material.elevation, 0.0);
expect(material.shadowColor, null);
expect(material.shape, const StadiumBorder());
expect(material.textStyle, null);
expect(material.type, MaterialType.button);
final Align align = tester.firstWidget<Align>(find.ancestor(of: find.byIcon(Icons.ac_unit), matching: find.byType(Align)));
expect(align.alignment, Alignment.center);
final Offset center = tester.getCenter(find.byType(IconButton));
final TestGesture gesture = await tester.startGesture(center);
await tester.pump(); // start the splash animation
await tester.pump(const Duration(milliseconds: 100)); // splash is underway
await gesture.up();
await tester.pumpAndSettle();
material = tester.widget<Material>(buttonMaterial);
// No change vs enabled and not pressed.
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, Colors.transparent);
expect(material.elevation, 0.0);
expect(material.shadowColor, null);
expect(material.shape, const StadiumBorder());
expect(material.textStyle, null);
expect(material.type, MaterialType.button);
// Disabled TextButton
await tester.pumpWidget(
MaterialApp(
theme: themeM3,
home: const Center(
child: IconButton(
onPressed: null,
icon: Icon(Icons.ac_unit),
),
),
),
);
material = tester.widget<Material>(buttonMaterial);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderOnForeground, true);
expect(material.borderRadius, null);
expect(material.clipBehavior, Clip.none);
expect(material.color, Colors.transparent);
expect(material.elevation, 0.0);
expect(material.shadowColor, null);
expect(material.shape, const StadiumBorder());
expect(material.textStyle, null);
expect(material.type, MaterialType.button);
});
testWidgets('Default IconButton meets a11y contrast guidelines - M3', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true),
home: Scaffold(
body: Center(
child: IconButton(
onPressed: () { },
focusNode: focusNode,
icon: const Icon(Icons.ac_unit),
),
),
),
),
);
// Default, not disabled.
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Hovered.
final Offset center = tester.getCenter(find.byType(IconButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
await expectLater(tester, meetsGuideline(textContrastGuideline));
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
await expectLater(tester, meetsGuideline(textContrastGuideline));
await gesture.removePointer();
},
skip: isBrowser, // https://github.com/flutter/flutter/issues/44115
);
testWidgets('IconButton uses stateful color for icon color in different states - M3', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
const Color pressedColor = Color(0x00000001);
const Color hoverColor = Color(0x00000002);
const Color focusedColor = Color(0x00000003);
const Color defaultColor = Color(0x00000004);
Color getIconColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return pressedColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.focused)) {
return focusedColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true),
home: Scaffold(
body: Center(
child: IconButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.resolveWith<Color>(getIconColor),
),
onPressed: () {},
focusNode: focusNode,
icon: const Icon(Icons.ac_unit),
),
),
),
),
);
Color? iconColor() => _iconStyle(tester, Icons.ac_unit)?.color;
// Default, not disabled.
expect(iconColor(), equals(defaultColor));
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(iconColor(), focusedColor);
// Hovered.
final Offset center = tester.getCenter(find.byType(IconButton));
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(iconColor(), hoverColor);
// Highlighted (pressed).
await gesture.down(center);
await tester.pump(); // Start the splash and highlight animations.
await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
expect(iconColor(), pressedColor);
});
testWidgets('Does IconButton contribute semantics - M3', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Theme(
data: ThemeData(useMaterial3: true),
child: IconButton(
style: const ButtonStyle(
// Specifying minimumSize to mimic the original minimumSize for
// RaisedButton so that the semantics tree's rect and transform
// match the original version of this test.
minimumSize: MaterialStatePropertyAll<Size>(Size(88, 36)),
),
onPressed: () { },
icon: const Icon(Icons.ac_unit),
),
),
),
),
);
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
actions: <SemanticsAction>[
SemanticsAction.tap,
],
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
transform: Matrix4.translationValues(356.0, 276.0, 0.0),
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
),
],
),
ignoreId: true,
));
semantics.dispose();
});
testWidgets('IconButton size is configurable by ThemeData.materialTapTargetSize - M3', (WidgetTester tester) async {
Widget buildFrame(MaterialTapTargetSize tapTargetSize) {
return Theme(
data: ThemeData(materialTapTargetSize: tapTargetSize, useMaterial3: true),
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: IconButton(
style: IconButton.styleFrom(minimumSize: const Size(40, 40)),
icon: const Icon(Icons.ac_unit),
onPressed: () { },
),
),
),
);
}
await tester.pumpWidget(buildFrame(MaterialTapTargetSize.padded));
expect(tester.getSize(find.byType(IconButton)), const Size(48.0, 48.0));
await tester.pumpWidget(buildFrame(MaterialTapTargetSize.shrinkWrap));
expect(tester.getSize(find.byType(IconButton)), const Size(40.0, 40.0));
});
testWidgets('Override IconButton default padding - M3', (WidgetTester tester) async {
// Use [IconButton]'s padding property to override default value.
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true),
home: Scaffold(
body: Center(
child: IconButton(
padding: const EdgeInsets.all(20),
onPressed: () {},
icon: const Icon(Icons.ac_unit),
),
),
),
)
);
final Padding paddingWidget1 = tester.widget<Padding>(
find.descendant(
of: find.byType(IconButton),
matching: find.byType(Padding),
),
);
expect(paddingWidget1.padding, const EdgeInsets.all(20));
// Use [IconButton.style]'s padding property to override default value.
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true),
home: Scaffold(
body: Center(
child: IconButton(
style: IconButton.styleFrom(padding: const EdgeInsets.all(20)),
onPressed: () {},
icon: const Icon(Icons.ac_unit),
),
),
),
)
);
final Padding paddingWidget2 = tester.widget<Padding>(
find.descendant(
of: find.byType(IconButton),
matching: find.byType(Padding),
),
);
expect(paddingWidget2.padding, const EdgeInsets.all(20));
// [IconButton.style]'s padding will override [IconButton]'s padding if both
// values are not null.
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true),
home: Scaffold(
body: Center(
child: IconButton(
padding: const EdgeInsets.all(15),
style: IconButton.styleFrom(padding: const EdgeInsets.all(22)),
onPressed: () {},
icon: const Icon(Icons.ac_unit),
),
),
),
)
);
final Padding paddingWidget3 = tester.widget<Padding>(
find.descendant(
of: find.byType(IconButton),
matching: find.byType(Padding),
),
);
expect(paddingWidget3.padding, const EdgeInsets.all(22));
});
} }
Widget wrap({ required Widget child }) { Widget wrap({required Widget child, required bool useMaterial3}) {
return FocusTraversalGroup( return useMaterial3
? MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true),
home: FocusTraversalGroup(
policy: ReadingOrderTraversalPolicy(),
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(child: child),
)),
)
: FocusTraversalGroup(
policy: ReadingOrderTraversalPolicy(), policy: ReadingOrderTraversalPolicy(),
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
...@@ -868,3 +1326,10 @@ Widget wrap({ required Widget child }) { ...@@ -868,3 +1326,10 @@ Widget wrap({ required Widget child }) {
), ),
); );
} }
TextStyle? _iconStyle(WidgetTester tester, IconData icon) {
final RichText iconRichText = tester.widget<RichText>(
find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)),
);
return iconRichText.text.style;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment