diff --git a/dev/tools/gen_defaults/bin/gen_defaults.dart b/dev/tools/gen_defaults/bin/gen_defaults.dart index d2b4fc53a0e..7228a0be5ab 100644 --- a/dev/tools/gen_defaults/bin/gen_defaults.dart +++ b/dev/tools/gen_defaults/bin/gen_defaults.dart @@ -22,6 +22,7 @@ import 'package:gen_defaults/button_template.dart'; import 'package:gen_defaults/card_template.dart'; import 'package:gen_defaults/dialog_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_rail_template.dart'; import 'package:gen_defaults/surface_tint.dart'; @@ -55,6 +56,9 @@ Future main(List args) async { 'fab_large_primary.json', 'fab_primary.json', 'fab_small_primary.json', + 'icon_button.json', + 'icon_button_filled.json', + 'icon_button_filled_tonal.json', 'motion.json', 'navigation_bar.json', 'navigation_rail.json', @@ -86,6 +90,7 @@ Future main(List args) async { CardTemplate('$materialLib/card.dart', tokens).updateFile(); DialogTemplate('$materialLib/dialog.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(); NavigationRailTemplate('$materialLib/navigation_rail.dart', tokens).updateFile(); SurfaceTintTemplate('$materialLib/elevation_overlay.dart', tokens).updateFile(); diff --git a/dev/tools/gen_defaults/lib/icon_button_template.dart b/dev/tools/gen_defaults/lib/icon_button_template.dart new file mode 100644 index 00000000000..c521958c076 --- /dev/null +++ b/dev/tools/gen_defaults/lib/icon_button_template.dart @@ -0,0 +1,99 @@ +// 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? get backgroundColor => + ButtonStyleButton.allOrNull(Colors.transparent); + + @override + MaterialStateProperty? get foregroundColor => + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) + return ${componentColor('md.comp.icon-button.disabled.icon')}; + return ${componentColor('md.comp.icon-button.unselected.icon')}; + }); + + @override + MaterialStateProperty? get overlayColor => + MaterialStateProperty.resolveWith((Set 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? get elevation => + ButtonStyleButton.allOrNull(0.0); + + @override + MaterialStateProperty? get padding => + ButtonStyleButton.allOrNull(const EdgeInsets.all(8.0)); + + @override + MaterialStateProperty? get minimumSize => + ButtonStyleButton.allOrNull(const Size(${tokens["md.comp.icon-button.state-layer.size"]}, ${tokens["md.comp.icon-button.state-layer.size"]})); + + // No default fixedSize + + @override + MaterialStateProperty? get maximumSize => + ButtonStyleButton.allOrNull(Size.infinite); + + // No default side + + @override + MaterialStateProperty? get shape => + ButtonStyleButton.allOrNull(${shape("md.comp.icon-button.state-layer")}); + + @override + MaterialStateProperty? get mouseCursor => + MaterialStateProperty.resolveWith((Set 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; +} +'''; + +} diff --git a/examples/api/lib/material/icon_button/icon_button.2.dart b/examples/api/lib/material/icon_button/icon_button.2.dart new file mode 100644 index 00000000000..95df1eb32cd --- /dev/null +++ b/examples/api/lib/material/icon_button/icon_button.2.dart @@ -0,0 +1,118 @@ +// 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 [ + 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: [ + 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 states) { + if (states.contains(MaterialState.pressed)) { + return colors.onSurface; + } + return null; + }), + ), + ), + ], + ), + ); + } +} diff --git a/packages/flutter/lib/src/material/icon_button.dart b/packages/flutter/lib/src/material/icon_button.dart index edec059e690..3b571ece718 100644 --- a/packages/flutter/lib/src/material/icon_button.dart +++ b/packages/flutter/lib/src/material/icon_button.dart @@ -8,11 +8,16 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.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 'debug.dart'; import 'icons.dart'; import 'ink_well.dart'; import 'material.dart'; +import 'material_state.dart'; import 'theme.dart'; import 'theme_data.dart'; import 'tooltip.dart'; @@ -92,6 +97,17 @@ const double _kMinButtonSize = kMinInteractiveDimension; /// ** See code in examples/api/lib/material/icon_button/icon_button.1.dart ** /// {@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: /// /// * [Icons], the library of Material Icons. @@ -134,6 +150,7 @@ class IconButton extends StatelessWidget { this.tooltip, this.enableFeedback = true, this.constraints, + this.style, required this.icon, }) : assert(padding != null), assert(alignment != null), @@ -184,6 +201,8 @@ class IconButton extends StatelessWidget { /// 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. final double? splashRadius; @@ -230,6 +249,8 @@ class IconButton extends StatelessWidget { /// 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. /// + /// If [ThemeData.useMaterial3] is set to true, this will not be used. + /// /// Defaults to the Theme's splash color, [ThemeData.splashColor]. final Color? splashColor; @@ -301,10 +322,125 @@ class IconButton extends StatelessWidget { /// and `Theme.of(context).visualDensity` otherwise. 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? buttonBackgroundColor = (backgroundColor == null && disabledBackgroundColor == null) + ? null + : _IconButtonDefaultBackground(backgroundColor, disabledBackgroundColor); + final MaterialStateProperty? buttonForegroundColor = (foregroundColor == null && disabledForegroundColor == null) + ? null + : _IconButtonDefaultForeground(foregroundColor, disabledForegroundColor); + final MaterialStateProperty? overlayColor = (foregroundColor == null && hoverColor == null && focusColor == null && highlightColor == null) + ? null + : _IconButtonDefaultOverlay(foregroundColor, focusColor, hoverColor, highlightColor); + final MaterialStateProperty? mouseCursor = (enabledMouseCursor == null && disabledMouseCursor == null) + ? null + : _IconButtonDefaultMouseCursor(enabledMouseCursor!, disabledMouseCursor!); + + return ButtonStyle( + backgroundColor: buttonBackgroundColor, + foregroundColor: buttonForegroundColor, + overlayColor: overlayColor, + shadowColor: ButtonStyleButton.allOrNull(shadowColor), + surfaceTintColor: ButtonStyleButton.allOrNull(surfaceTintColor), + elevation: ButtonStyleButton.allOrNull(elevation), + padding: ButtonStyleButton.allOrNull(padding), + minimumSize: ButtonStyleButton.allOrNull(minimumSize), + fixedSize: ButtonStyleButton.allOrNull(fixedSize), + maximumSize: ButtonStyleButton.allOrNull(maximumSize), + side: ButtonStyleButton.allOrNull(side), + shape: ButtonStyleButton.allOrNull(shape), + mouseCursor: mouseCursor, + visualDensity: visualDensity, + tapTargetSize: tapTargetSize, + animationDuration: animationDuration, + enableFeedback: enableFeedback, + alignment: alignment, + splashFactory: splashFactory, + ); + } + @override Widget build(BuildContext context) { - assert(debugCheckHasMaterial(context)); final ThemeData theme = Theme.of(context); + if (!theme.useMaterial3) { + assert(debugCheckHasMaterial(context)); + } + Color? currentColor; if (onPressed != null) { currentColor = color; @@ -321,6 +457,55 @@ class IconButton extends StatelessWidget { final BoxConstraints adjustedConstraints = effectiveVisualDensity.effectiveConstraints(unadjustedConstraints); 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( constraints: adjustedConstraints, child: Padding( @@ -389,3 +574,245 @@ class IconButton extends StatelessWidget { properties.add(DiagnosticsProperty('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 { + _IconButtonDefaultBackground(this.background, this.disabledBackground); + + final Color? background; + final Color? disabledBackground; + + @override + Color? resolve(Set states) { + if (states.contains(MaterialState.disabled)) { + return disabledBackground; + } + return background; + } + + @override + String toString() { + return '{disabled: $disabledBackground, otherwise: $background}'; + } +} + +@immutable +class _IconButtonDefaultForeground extends MaterialStateProperty { + _IconButtonDefaultForeground(this.foregroundColor, this.disabledForegroundColor); + + final Color? foregroundColor; + final Color? disabledForegroundColor; + + @override + Color? resolve(Set states) { + if (states.contains(MaterialState.disabled)) { + return disabledForegroundColor; + } + return foregroundColor; + } + + @override + String toString() { + return '{disabled: $disabledForegroundColor, otherwise: $foregroundColor}'; + } +} + +@immutable +class _IconButtonDefaultOverlay extends MaterialStateProperty { + _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 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 with Diagnosticable { + _IconButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor); + + final MouseCursor enabledCursor; + final MouseCursor disabledCursor; + + @override + MouseCursor resolve(Set 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? get backgroundColor => + ButtonStyleButton.allOrNull(Colors.transparent); + + @override + MaterialStateProperty? get foregroundColor => + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return _colors.onSurface.withOpacity(0.38); + } + return _colors.onSurfaceVariant; + }); + + @override + MaterialStateProperty? get overlayColor => + MaterialStateProperty.resolveWith((Set 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? get elevation => + ButtonStyleButton.allOrNull(0.0); + + @override + MaterialStateProperty? get padding => + ButtonStyleButton.allOrNull(const EdgeInsets.all(8.0)); + + @override + MaterialStateProperty? get minimumSize => + ButtonStyleButton.allOrNull(const Size(40.0, 40.0)); + + // No default fixedSize + + @override + MaterialStateProperty? get maximumSize => + ButtonStyleButton.allOrNull(Size.infinite); + + // No default side + + @override + MaterialStateProperty? get shape => + ButtonStyleButton.allOrNull(const StadiumBorder()); + + @override + MaterialStateProperty? get mouseCursor => + MaterialStateProperty.resolveWith((Set 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 diff --git a/packages/flutter/test/material/icon_button_test.dart b/packages/flutter/test/material/icon_button_test.dart index 3c2a596e8d9..9f1d6ccfd19 100644 --- a/packages/flutter/test/material/icon_button_test.dart +++ b/packages/flutter/test/material/icon_button_test.dart @@ -22,18 +22,21 @@ class MockOnPressedFunction { void main() { late MockOnPressedFunction mockOnPressedFunction; - + const ColorScheme colorScheme = ColorScheme.light(); + final ThemeData theme = ThemeData.from(colorScheme: colorScheme); setUp(() { mockOnPressedFunction = MockOnPressedFunction(); }); testWidgets('test default icon buttons are sized up to 48', (WidgetTester tester) async { + final bool material3 = theme.useMaterial3; await tester.pumpWidget( wrap( - child: IconButton( - onPressed: mockOnPressedFunction.handler, - icon: const Icon(Icons.link), - ), + useMaterial3: material3, + child: IconButton( + onPressed: mockOnPressedFunction.handler, + icon: const Icon(Icons.link), + ), ), ); @@ -45,14 +48,16 @@ void main() { }); testWidgets('test small icons are sized up to 48dp', (WidgetTester tester) async { + final bool material3 = theme.useMaterial3; await tester.pumpWidget( wrap( - child: IconButton( - iconSize: 10.0, - onPressed: mockOnPressedFunction.handler, - icon: const Icon(Icons.link), - ), - ), + useMaterial3: material3, + child: IconButton( + iconSize: 10.0, + onPressed: mockOnPressedFunction.handler, + icon: const Icon(Icons.link), + ), + ) ); final RenderBox iconButton = tester.renderObject(find.byType(IconButton)); @@ -60,15 +65,17 @@ void main() { }); testWidgets('test icons can be small when total size is >48dp', (WidgetTester tester) async { + final bool material3 = theme.useMaterial3; await tester.pumpWidget( wrap( - child: IconButton( - iconSize: 10.0, - padding: const EdgeInsets.all(30.0), - onPressed: mockOnPressedFunction.handler, - icon: const Icon(Icons.link), - ), - ), + useMaterial3: material3, + child: IconButton( + iconSize: 10.0, + padding: const EdgeInsets.all(30.0), + onPressed: mockOnPressedFunction.handler, + icon: const Icon(Icons.link), + ), + ) ); final RenderBox iconButton = tester.renderObject(find.byType(IconButton)); @@ -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 { - final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); + final bool material3 = theme.useMaterial3; await tester.pumpWidget( wrap( - child: IconTheme( - data: const IconThemeData(), - child: IconButton( - focusNode: focusNode, - onPressed: mockOnPressedFunction.handler, - icon: const Icon(Icons.link), - ), - ) - ), + useMaterial3: material3, + child: IconTheme( + data: const IconThemeData(), + child: IconButton( + focusNode: FocusNode(debugLabel: 'Ink Focus'), + onPressed: mockOnPressedFunction.handler, + icon: const Icon(Icons.link), + ), + ) + ) ); final RenderBox icon = tester.renderObject(find.byType(Icon)); @@ -96,9 +104,11 @@ void main() { testWidgets('when null, iconSize is overridden by closest IconTheme', (WidgetTester tester) async { RenderBox icon; + final bool material3 = theme.useMaterial3; await tester.pumpWidget( wrap( + useMaterial3: material3, child: IconTheme( data: const IconThemeData(size: 10), child: IconButton( @@ -106,6 +116,47 @@ void main() { icon: const Icon(Icons.link), ), ) + ) + ); + + icon = tester.renderObject(find.byType(Icon)); + expect(icon.size, const Size(10.0, 10.0)); + + await tester.pumpWidget( + wrap( + useMaterial3: material3, + child: Theme( + data: ThemeData( + useMaterial3: material3, + iconTheme: const IconThemeData(size: 10), + ), + child: IconButton( + onPressed: mockOnPressedFunction.handler, + icon: const Icon(Icons.link), + ), + ) + ) + ); + + icon = tester.renderObject(find.byType(Icon)); + expect(icon.size, const Size(10.0, 10.0)); + + await tester.pumpWidget( + wrap( + useMaterial3: material3, + child: Theme( + data: ThemeData( + useMaterial3: material3, + iconTheme: const IconThemeData(size: 20), + ), + child: IconTheme( + data: const IconThemeData(size: 10), + child: IconButton( + onPressed: mockOnPressedFunction.handler, + icon: const Icon(Icons.link), + ), + ), + ) ), ); @@ -114,55 +165,20 @@ void main() { await tester.pumpWidget( wrap( + useMaterial3: material3, + child: IconTheme( + data: const IconThemeData(size: 20), child: Theme( data: ThemeData( + useMaterial3: material3, iconTheme: const IconThemeData(size: 10), ), child: IconButton( onPressed: mockOnPressedFunction.handler, icon: const Icon(Icons.link), ), - ) - ), - ); - - icon = tester.renderObject(find.byType(Icon)); - expect(icon.size, const Size(10.0, 10.0)); - - await tester.pumpWidget( - wrap( - child: Theme( - data: ThemeData( - iconTheme: const IconThemeData(size: 20), - ), - child: IconTheme( - data: const IconThemeData(size: 10), - child: IconButton( - onPressed: mockOnPressedFunction.handler, - icon: const Icon(Icons.link), - ), - ), - ) - ), - ); - - icon = tester.renderObject(find.byType(Icon)); - expect(icon.size, const Size(10.0, 10.0)); - - await tester.pumpWidget( - wrap( - child: IconTheme( - data: const IconThemeData(size: 20), - child: Theme( - data: ThemeData( - iconTheme: const IconThemeData(size: 10), - ), - child: IconButton( - onPressed: mockOnPressedFunction.handler, - icon: const Icon(Icons.link), - ), - ), - ) + ), + ) ), ); @@ -171,16 +187,18 @@ void main() { }); testWidgets('when non-null, iconSize precedes IconTheme.of(context).size', (WidgetTester tester) async { + final bool material3 = theme.useMaterial3; await tester.pumpWidget( wrap( - child: IconTheme( - data: const IconThemeData(size: 30.0), - child: IconButton( - iconSize: 10.0, - onPressed: mockOnPressedFunction.handler, - icon: const Icon(Icons.link), - ), - ) + useMaterial3: material3, + child: IconTheme( + data: const IconThemeData(size: 30.0), + child: IconButton( + iconSize: 10.0, + onPressed: mockOnPressedFunction.handler, + icon: const Icon(Icons.link), + ), + ) ), ); @@ -188,28 +206,36 @@ void main() { 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( wrap( + useMaterial3: material3, child: IconButton( iconSize: 10.0, onPressed: mockOnPressedFunction.handler, icon: const Icon(Icons.link), constraints: const BoxConstraints(), - ), - ), + ) + ) ); 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 // 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 { + final bool material3 = theme.useMaterial3; await tester.pumpWidget( wrap( + useMaterial3: material3, child: IconButton( iconSize: 10.0, padding: const EdgeInsets.all(3.0), @@ -221,17 +247,23 @@ void main() { ); 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 // 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 { + final bool material3 = theme.useMaterial3; await tester.pumpWidget( wrap( + useMaterial3: material3, child: Theme( - data: ThemeData(visualDensity: const VisualDensity(horizontal: 1, vertical: -1)), + data: ThemeData(visualDensity: const VisualDensity(horizontal: 1, vertical: -1), useMaterial3: material3), child: IconButton( iconSize: 10.0, onPressed: mockOnPressedFunction.handler, @@ -248,18 +280,19 @@ void main() { // width by 4 pixels and decreases its height by 4 pixels, giving // final width 32.0 + 4.0 = 36.0 and // 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 { await tester.pumpWidget( wrap( - child: IconButton( - padding: EdgeInsets.zero, - onPressed: mockOnPressedFunction.handler, - icon: const Icon(Icons.ac_unit), - iconSize: 80.0, - ), + useMaterial3: theme.useMaterial3, + child: IconButton( + padding: EdgeInsets.zero, + onPressed: mockOnPressedFunction.handler, + icon: const Icon(Icons.ac_unit), + iconSize: 80.0, + ), ), ); @@ -287,16 +320,39 @@ void main() { final RenderBox box = tester.renderObject(find.byType(IconButton)); 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: [ + 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 { await tester.pumpWidget( wrap( - child: IconButton( - onPressed: mockOnPressedFunction.handler, - icon: const Icon(Icons.ac_unit), - iconSize: 80.0, - ), + useMaterial3: theme.useMaterial3, + child: IconButton( + onPressed: mockOnPressedFunction.handler, + icon: const Icon(Icons.ac_unit), + iconSize: 80.0, + ), ), ); @@ -307,6 +363,7 @@ void main() { testWidgets('test tooltip', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( + theme: theme, home: Material( child: Center( child: IconButton( @@ -325,6 +382,7 @@ void main() { await tester.pumpWidget( MaterialApp( + theme: theme, home: Material( child: Center( child: IconButton( @@ -347,6 +405,7 @@ void main() { testWidgets('IconButton AppBar size', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( + theme: theme, home: Scaffold( appBar: AppBar( actions: [ @@ -368,22 +427,23 @@ void main() { // 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. - 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 directHighlightColor = Color(0xFF0000F0); Widget buttonWidget = wrap( - child: IconButton( - icon: const Icon(Icons.android), - splashColor: directSplashColor, - highlightColor: directHighlightColor, - onPressed: () { /* enable the button */ }, - ), + useMaterial3: false, + child: IconButton( + icon: const Icon(Icons.android), + splashColor: directSplashColor, + highlightColor: directHighlightColor, + onPressed: () { /* enable the button */ }, + ), ); await tester.pumpWidget( Theme( - data: ThemeData(), + data: ThemeData(useMaterial3: false), child: buttonWidget, ), ); @@ -404,10 +464,11 @@ void main() { const Color themeHighlightColor1 = Color(0xFF00FF00); buttonWidget = wrap( - child: IconButton( - icon: const Icon(Icons.android), - onPressed: () { /* enable the button */ }, - ), + useMaterial3: false, + child: IconButton( + icon: const Icon(Icons.android), + onPressed: () { /* enable the button */ }, + ), ); await tester.pumpWidget( @@ -415,6 +476,7 @@ void main() { data: ThemeData( highlightColor: themeHighlightColor1, splashColor: themeSplashColor1, + useMaterial3: false, ), child: buttonWidget, ), @@ -435,6 +497,7 @@ void main() { data: ThemeData( highlightColor: themeHighlightColor2, splashColor: themeSplashColor2, + useMaterial3: false, ), child: buttonWidget, // same widget, so does not get updated because of us ), @@ -450,10 +513,11 @@ void main() { 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; await tester.pumpWidget( MaterialApp( + theme: ThemeData(useMaterial3: false), home: Material( child: Center( child: IconButton( @@ -480,11 +544,12 @@ void main() { await gesture.up(); }); - testWidgets('IconButton Semantics (enabled)', (WidgetTester tester) async { + testWidgets('IconButton Semantics (enabled) - M2', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( wrap( + useMaterial3: false, child: IconButton( onPressed: mockOnPressedFunction.handler, icon: const Icon(Icons.link, semanticLabel: 'link'), @@ -513,11 +578,12 @@ void main() { semantics.dispose(); }); - testWidgets('IconButton Semantics (disabled)', (WidgetTester tester) async { + testWidgets('IconButton Semantics (disabled) - M2', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( wrap( + useMaterial3: false, child: const IconButton( onPressed: null, icon: Icon(Icons.link, semanticLabel: 'link'), @@ -545,6 +611,7 @@ void main() { final FocusNode focusNode = FocusNode(debugLabel: 'IconButton'); await tester.pumpWidget( wrap( + useMaterial3: theme.useMaterial3, child: IconButton( focusNode: focusNode, autofocus: true, @@ -559,6 +626,7 @@ void main() { await tester.pumpWidget( wrap( + useMaterial3: theme.useMaterial3, child: IconButton( focusNode: focusNode, autofocus: true, @@ -575,6 +643,7 @@ void main() { final FocusNode focusNode = FocusNode(debugLabel: 'IconButton'); await tester.pumpWidget( wrap( + useMaterial3: theme.useMaterial3, child: MediaQuery( data: const MediaQueryData( navigationMode: NavigationMode.directional, @@ -594,6 +663,7 @@ void main() { await tester.pumpWidget( wrap( + useMaterial3: theme.useMaterial3, child: MediaQuery( data: const MediaQueryData( navigationMode: NavigationMode.directional, @@ -617,6 +687,7 @@ void main() { await tester.pumpWidget( wrap( + useMaterial3: theme.useMaterial3, child: Column( children: [ IconButton( @@ -658,18 +729,22 @@ void main() { }); testWidgets('IconButton with disabled feedback', (WidgetTester tester) async { - await tester.pumpWidget(Material( - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: IconButton( - onPressed: () {}, - enableFeedback: false, - icon: const Icon(Icons.link), - ), + final Widget button = Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: IconButton( + onPressed: () {}, + enableFeedback: false, + 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.pump(const Duration(seconds: 1)); expect(feedback.clickSoundCount, 0); @@ -677,17 +752,21 @@ void main() { }); testWidgets('IconButton with enabled feedback', (WidgetTester tester) async { - await tester.pumpWidget(Material( - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: IconButton( - onPressed: () {}, - icon: const Icon(Icons.link), - ), + final Widget button = Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: IconButton( + onPressed: () {}, + 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.pump(const Duration(seconds: 1)); expect(feedback.clickSoundCount, 1); @@ -695,17 +774,21 @@ void main() { }); testWidgets('IconButton with enabled feedback by default', (WidgetTester tester) async { - await tester.pumpWidget(Material( - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: IconButton( - onPressed: () {}, - icon: const Icon(Icons.link), - ), + final Widget button = Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: IconButton( + onPressed: () {}, + 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.pump(const Duration(seconds: 1)); expect(feedback.clickSoundCount, 1); @@ -715,9 +798,11 @@ void main() { testWidgets('IconButton responds to density changes.', (WidgetTester tester) async { const Key key = Key('test'); + final bool material3 = theme.useMaterial3; Future buildTest(VisualDensity visualDensity) async { return tester.pumpWidget( MaterialApp( + theme: theme, home: Material( child: Center( child: IconButton( @@ -733,34 +818,42 @@ void main() { } await buildTest(VisualDensity.standard); - final RenderBox box = tester.renderObject(find.byKey(key)); + final RenderBox box = tester.renderObject(find.byType(IconButton)); await tester.pumpAndSettle(); expect(box.size, equals(const Size(48, 48))); await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0)); 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 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 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 { // Test argument works await tester.pumpWidget( - Material( - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: IconButton( - onPressed: () {}, - mouseCursor: SystemMouseCursors.forbidden, - icon: const Icon(Icons.play_arrow), + MaterialApp( + theme: theme, + home: Material( + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: IconButton( + onPressed: () {}, + mouseCursor: SystemMouseCursors.forbidden, + icon: const Icon(Icons.play_arrow), + ), ), ), ), @@ -776,13 +869,16 @@ void main() { // Test default is click await tester.pumpWidget( - Material( - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: IconButton( - onPressed: () {}, - icon: const Icon(Icons.play_arrow), + MaterialApp( + theme: theme, + home: Material( + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: IconButton( + onPressed: () {}, + icon: const Icon(Icons.play_arrow), + ), ), ), ), @@ -794,13 +890,16 @@ void main() { testWidgets('disabled IconButton has basic mouse cursor', (WidgetTester tester) async { await tester.pumpWidget( - const Material( - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: IconButton( - onPressed: null, // null value indicates IconButton is disabled - icon: Icon(Icons.play_arrow), + MaterialApp( + theme: theme, + home: const Material( + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: IconButton( + onPressed: null, // null value indicates IconButton is disabled + icon: Icon(Icons.play_arrow), + ), ), ), ), @@ -817,14 +916,17 @@ void main() { testWidgets('IconButton.mouseCursor overrides implicit setting of mouse cursor', (WidgetTester tester) async { await tester.pumpWidget( - const Material( - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: IconButton( - onPressed: null, - mouseCursor: SystemMouseCursors.none, - icon: Icon(Icons.play_arrow), + MaterialApp( + theme: theme, + home: const Material( + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: IconButton( + onPressed: null, + mouseCursor: SystemMouseCursors.none, + icon: Icon(Icons.play_arrow), + ), ), ), ), @@ -839,14 +941,17 @@ void main() { expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.none); await tester.pumpWidget( - Material( - child: Directionality( - textDirection: TextDirection.ltr, - child: Center( - child: IconButton( - onPressed: () {}, - mouseCursor: SystemMouseCursors.none, - icon: const Icon(Icons.play_arrow), + MaterialApp( + theme: theme, + home: Material( + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: IconButton( + onPressed: () {}, + mouseCursor: SystemMouseCursors.none, + icon: const Icon(Icons.play_arrow), + ), ), ), ), @@ -855,16 +960,376 @@ void main() { 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(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(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(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(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 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(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(88, 36)), + ), + onPressed: () { }, + icon: const Icon(Icons.ac_unit), + ), + ), + ), + ), + ); + + expect(semantics, hasSemantics( + TestSemantics.root( + children: [ + TestSemantics.rootChild( + actions: [ + 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.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( + 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( + 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( + find.descendant( + of: find.byType(IconButton), + matching: find.byType(Padding), + ), + ); + expect(paddingWidget3.padding, const EdgeInsets.all(22)); + }); } -Widget wrap({ required Widget child }) { - return FocusTraversalGroup( - policy: ReadingOrderTraversalPolicy(), - child: Directionality( - textDirection: TextDirection.ltr, - child: Material( - child: Center(child: child), - ), - ), - ); +Widget wrap({required Widget child, required bool useMaterial3}) { + 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(), + child: Directionality( + textDirection: TextDirection.ltr, + child: Material( + child: Center(child: child), + ), + ), + ); +} + +TextStyle? _iconStyle(WidgetTester tester, IconData icon) { + final RichText iconRichText = tester.widget( + find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)), + ); + return iconRichText.text.style; }