From 10442399fbebfb883eb01a106e8cc6303e1fcf4c Mon Sep 17 00:00:00 2001 From: Tirth Date: Mon, 12 Feb 2024 22:38:20 +0530 Subject: [PATCH] Introduce `iconAlignment` for the buttons with icon (#137348) Adds `iconAlignment` property to `ButtonStyleButton` widget. Fixes #89564 ### Example https://github.com/flutter/flutter/assets/13456345/1b5236c4-5c60-4915-b3c6-0a56c43f8a19 --- .../button_style_button.icon_alignment.0.dart | 147 ++++++++++++ ...on_style_button.icon_alignment.0_test.dart | 102 ++++++++ .../lib/src/material/button_style_button.dart | 39 +++ .../lib/src/material/elevated_button.dart | 26 +- .../lib/src/material/filled_button.dart | 36 ++- .../lib/src/material/outlined_button.dart | 22 +- .../flutter/lib/src/material/text_button.dart | 22 +- .../test/material/elevated_button_test.dart | 112 +++++++++ .../test/material/filled_button_test.dart | 225 +++++++++++++++++- .../test/material/outlined_button_test.dart | 114 ++++++++- .../test/material/text_button_test.dart | 114 ++++++++- 11 files changed, 942 insertions(+), 17 deletions(-) create mode 100644 examples/api/lib/material/button_style_button/button_style_button.icon_alignment.0.dart create mode 100644 examples/api/test/material/button_style_button/button_style_button.icon_alignment.0_test.dart diff --git a/examples/api/lib/material/button_style_button/button_style_button.icon_alignment.0.dart b/examples/api/lib/material/button_style_button/button_style_button.icon_alignment.0.dart new file mode 100644 index 00000000000..1b19ca629e6 --- /dev/null +++ b/examples/api/lib/material/button_style_button/button_style_button.icon_alignment.0.dart @@ -0,0 +1,147 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Flutter code sample for using [ButtonStyleButton.iconAlignment] parameter. + +void main() { + runApp(const ButtonStyleButtonIconAlignmentApp()); +} + +class ButtonStyleButtonIconAlignmentApp extends StatelessWidget { + const ButtonStyleButtonIconAlignmentApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: Scaffold( + body: ButtonStyleButtonIconAlignmentExample(), + ), + ); + } +} + +class ButtonStyleButtonIconAlignmentExample extends StatefulWidget { + const ButtonStyleButtonIconAlignmentExample({super.key}); + + @override + State createState() => _ButtonStyleButtonIconAlignmentExampleState(); +} + +class _ButtonStyleButtonIconAlignmentExampleState extends State { + TextDirection _textDirection = TextDirection.ltr; + IconAlignment _iconAlignment = IconAlignment.start; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Directionality( + key: const Key('Directionality'), + textDirection: _textDirection, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Spacer(), + OverflowBar( + spacing: 10, + overflowSpacing: 20, + alignment: MainAxisAlignment.center, + overflowAlignment: OverflowBarAlignment.center, + children: [ + ElevatedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.sunny), + label: const Text('ElevatedButton'), + iconAlignment: _iconAlignment, + ), + FilledButton.icon( + onPressed: () {}, + icon: const Icon(Icons.beach_access), + label: const Text('FilledButton'), + iconAlignment: _iconAlignment, + ), + FilledButton.tonalIcon( + onPressed: () {}, + icon: const Icon(Icons.cloud), + label: const Text('FilledButton Tonal'), + iconAlignment: _iconAlignment, + ), + OutlinedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.light), + label: const Text('OutlinedButton'), + iconAlignment: _iconAlignment, + ), + TextButton.icon( + onPressed: () {}, + icon: const Icon(Icons.flight_takeoff), + label: const Text('TextButton'), + iconAlignment: _iconAlignment, + ), + ], + ), + const Spacer(), + OverflowBar( + alignment: MainAxisAlignment.spaceEvenly, + overflowAlignment: OverflowBarAlignment.center, + spacing: 10, + overflowSpacing: 10, + children: [ + Column( + children: [ + const Text('Icon alignment'), + const SizedBox(height: 10), + SegmentedButton( + onSelectionChanged: (Set value) { + setState(() { + _iconAlignment = value.first; + }); + }, + selected: { _iconAlignment }, + segments: IconAlignment.values.map((IconAlignment iconAlignment) { + return ButtonSegment( + value: iconAlignment, + label: Text(iconAlignment.name), + ); + }).toList(), + ), + ], + ), + Column( + children: [ + const Text('Text direction'), + const SizedBox(height: 10), + SegmentedButton( + onSelectionChanged: (Set value) { + setState(() { + _textDirection = value.first; + }); + }, + selected: { _textDirection }, + segments: const >[ + ButtonSegment( + value: TextDirection.ltr, + label: Text('LTR'), + ), + ButtonSegment( + value: TextDirection.rtl, + label: Text('RTL'), + ), + ], + ), + ], + ), + ], + ), + const Spacer(), + ], + ), + ), + ), + ); + } +} diff --git a/examples/api/test/material/button_style_button/button_style_button.icon_alignment.0_test.dart b/examples/api/test/material/button_style_button/button_style_button.icon_alignment.0_test.dart new file mode 100644 index 00000000000..2895d22e6d4 --- /dev/null +++ b/examples/api/test/material/button_style_button/button_style_button.icon_alignment.0_test.dart @@ -0,0 +1,102 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/button_style_button/button_style_button.icon_alignment.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('ButtonStyleButton.iconAlignment updates button icons alignment', (WidgetTester tester) async { + await tester.pumpWidget( + const example.ButtonStyleButtonIconAlignmentApp(), + ); + + Finder findButtonMaterial(String text) { + return find.ancestor( + of: find.text(text), + matching: find.byType(Material), + ).first; + } + + void expectedLeftIconPosition({ + required double iconOffset, + required double textButtonIconOffset, + }) { + expect( + tester.getTopLeft(findButtonMaterial('ElevatedButton')).dx, + tester.getTopLeft(find.byIcon(Icons.sunny)).dx - iconOffset, + ); + expect( + tester.getTopLeft(findButtonMaterial('FilledButton')).dx, + tester.getTopLeft(find.byIcon(Icons.beach_access)).dx - iconOffset, + ); + expect( + tester.getTopLeft(findButtonMaterial('FilledButton Tonal')).dx, + tester.getTopLeft(find.byIcon(Icons.cloud)).dx - iconOffset, + ); + expect( + tester.getTopLeft(findButtonMaterial('OutlinedButton')).dx, + tester.getTopLeft(find.byIcon(Icons.light)).dx - iconOffset, + ); + expect( + tester.getTopLeft(findButtonMaterial('TextButton')).dx, + tester.getTopLeft(find.byIcon(Icons.flight_takeoff)).dx - textButtonIconOffset, + ); + } + + void expectedRightIconPosition({ + required double iconOffset, + required double textButtonIconOffset, + }) { + expect( + tester.getTopRight(findButtonMaterial('ElevatedButton')).dx, + tester.getTopRight(find.byIcon(Icons.sunny)).dx + iconOffset, + ); + expect( + tester.getTopRight(findButtonMaterial('FilledButton')).dx, + tester.getTopRight(find.byIcon(Icons.beach_access)).dx + iconOffset, + ); + expect( + tester.getTopRight(findButtonMaterial('FilledButton Tonal')).dx, + tester.getTopRight(find.byIcon(Icons.cloud)).dx + iconOffset, + ); + expect( + tester.getTopRight(findButtonMaterial('OutlinedButton')).dx, + tester.getTopRight(find.byIcon(Icons.light)).dx + iconOffset, + ); + expect( + tester.getTopRight(findButtonMaterial('TextButton')).dx, + tester.getTopRight(find.byIcon(Icons.flight_takeoff)).dx + textButtonIconOffset, + ); + } + + // Test initial icon alignment in LTR. + expectedLeftIconPosition(iconOffset: 16, textButtonIconOffset: 12); + + // Update icon alignment to end. + await tester.tap(find.text('end')); + await tester.pumpAndSettle(); + + // Test icon alignment end in LTR. + expectedRightIconPosition(iconOffset: 24, textButtonIconOffset: 16); + + // Reset icon alignment to start. + await tester.tap(find.text('start')); + await tester.pumpAndSettle(); + + // Change text direction to RTL. + await tester.tap(find.text('RTL')); + await tester.pumpAndSettle(); + + // Test icon alignment start in LTR. + expectedRightIconPosition(iconOffset: 16, textButtonIconOffset: 12); + + // Update icon alignment to end. + await tester.tap(find.text('end')); + await tester.pumpAndSettle(); + + // Test icon alignment end in LTR. + expectedLeftIconPosition(iconOffset: 24, textButtonIconOffset: 16); + }); +} diff --git a/packages/flutter/lib/src/material/button_style_button.dart b/packages/flutter/lib/src/material/button_style_button.dart index 5b35eb583cd..47ee2927c06 100644 --- a/packages/flutter/lib/src/material/button_style_button.dart +++ b/packages/flutter/lib/src/material/button_style_button.dart @@ -11,11 +11,46 @@ import 'package:flutter/widgets.dart'; import 'button_style.dart'; import 'colors.dart'; import 'constants.dart'; +import 'elevated_button.dart'; +import 'filled_button.dart'; import 'ink_well.dart'; import 'material.dart'; import 'material_state.dart'; +import 'outlined_button.dart'; +import 'text_button.dart'; import 'theme_data.dart'; +/// {@template flutter.material.ButtonStyleButton.iconAlignment} +/// Determines the alignment of the icon within the widgets such as: +/// - [ElevatedButton.icon], +/// - [FilledButton.icon], +/// - [FilledButton.tonalIcon]. +/// - [OutlinedButton.icon], +/// - [TextButton.icon], +/// +/// The effect of `iconAlignment` depends on [TextDirection]. If textDirection is +/// [TextDirection.ltr] then [IconAlignment.start] and [IconAlignment.end] align the +/// icon on the left or right respectively. If textDirection is [TextDirection.rtl] the +/// the alignments are reversed. +/// +/// Defaults to [IconAlignment.start]. +/// +/// {@tool dartpad} +/// This sample demonstrates how to use `iconAlignment` to align the button icon to the start +/// or the end of the button. +/// +/// ** See code in examples/api/lib/material/button_style_button/button_style_button.icon_alignment.0.dart ** +/// {@end-tool} +/// +/// {@endtemplate} +enum IconAlignment { + /// The icon is placed at the start of the button. + start, + + /// The icon is placed at the end of the button. + end, +} + /// The base [StatefulWidget] class for buttons whose style is defined by a [ButtonStyle] object. /// /// Concrete subclasses must override [defaultStyleOf] and [themeStyleOf]. @@ -44,6 +79,7 @@ abstract class ButtonStyleButton extends StatefulWidget { this.statesController, this.isSemanticButton = true, required this.child, + this.iconAlignment = IconAlignment.start, }); /// Called when the button is tapped or otherwise activated. @@ -117,6 +153,9 @@ abstract class ButtonStyleButton extends StatefulWidget { /// {@macro flutter.widgets.ProxyWidget.child} final Widget? child; + /// {@macro flutter.material.ButtonStyleButton.iconAlignment} + final IconAlignment iconAlignment; + /// Returns a non-null [ButtonStyle] that's based primarily on the [Theme]'s /// [ThemeData.textTheme] and [ThemeData.colorScheme]. /// diff --git a/packages/flutter/lib/src/material/elevated_button.dart b/packages/flutter/lib/src/material/elevated_button.dart index 802a482ea70..3380abd9d68 100644 --- a/packages/flutter/lib/src/material/elevated_button.dart +++ b/packages/flutter/lib/src/material/elevated_button.dart @@ -74,6 +74,7 @@ class ElevatedButton extends ButtonStyleButton { super.clipBehavior, super.statesController, required super.child, + super.iconAlignment, }); /// Create an elevated button from a pair of widgets that serve as the button's @@ -83,6 +84,9 @@ class ElevatedButton extends ButtonStyleButton { /// at the start, and 16 at the end, with an 8 pixel gap in between. /// /// If [icon] is null, will create an [ElevatedButton] instead. + /// + /// {@macro flutter.material.ButtonStyleButton.iconAlignment} + /// factory ElevatedButton.icon({ Key? key, required VoidCallback? onPressed, @@ -96,6 +100,7 @@ class ElevatedButton extends ButtonStyleButton { MaterialStatesController? statesController, Widget? icon, required Widget label, + IconAlignment iconAlignment = IconAlignment.start, }) { if (icon == null) { return ElevatedButton( @@ -125,6 +130,7 @@ class ElevatedButton extends ButtonStyleButton { statesController: statesController, icon: icon, label: label, + iconAlignment: iconAlignment, ); } @@ -532,9 +538,15 @@ class _ElevatedButtonWithIcon extends ElevatedButton { super.statesController, required Widget icon, required Widget label, + super.iconAlignment, }) : super( autofocus: autofocus ?? false, - child: _ElevatedButtonWithIconChild(icon: icon, label: label, buttonStyle: style), + child: _ElevatedButtonWithIconChild( + icon: icon, + label: label, + buttonStyle: style, + iconAlignment: iconAlignment, + ), ); @override @@ -563,11 +575,17 @@ class _ElevatedButtonWithIcon extends ElevatedButton { } class _ElevatedButtonWithIconChild extends StatelessWidget { - const _ElevatedButtonWithIconChild({ required this.label, required this.icon, required this.buttonStyle }); + const _ElevatedButtonWithIconChild({ + required this.label, + required this.icon, + required this.buttonStyle, + required this.iconAlignment, + }); final Widget label; final Widget icon; final ButtonStyle? buttonStyle; + final IconAlignment iconAlignment; @override Widget build(BuildContext context) { @@ -576,7 +594,9 @@ class _ElevatedButtonWithIconChild extends StatelessWidget { final double gap = lerpDouble(8, 4, scale)!; return Row( mainAxisSize: MainAxisSize.min, - children: [icon, SizedBox(width: gap), Flexible(child: label)], + children: iconAlignment == IconAlignment.start + ? [icon, SizedBox(width: gap), Flexible(child: label)] + : [Flexible(child: label), SizedBox(width: gap), icon], ); } } diff --git a/packages/flutter/lib/src/material/filled_button.dart b/packages/flutter/lib/src/material/filled_button.dart index 2a09f10780e..438aade9c37 100644 --- a/packages/flutter/lib/src/material/filled_button.dart +++ b/packages/flutter/lib/src/material/filled_button.dart @@ -75,6 +75,7 @@ class FilledButton extends ButtonStyleButton { super.clipBehavior = Clip.none, super.statesController, required super.child, + super.iconAlignment, }) : _variant = _FilledButtonVariant.filled; /// Create a filled button from [icon] and [label]. @@ -83,6 +84,9 @@ class FilledButton extends ButtonStyleButton { /// and a gap between them. /// /// If [icon] is null, will create a [FilledButton] instead. + /// + /// {@macro flutter.material.ButtonStyleButton.iconAlignment} + /// factory FilledButton.icon({ Key? key, required VoidCallback? onPressed, @@ -96,6 +100,7 @@ class FilledButton extends ButtonStyleButton { MaterialStatesController? statesController, Widget? icon, required Widget label, + IconAlignment iconAlignment = IconAlignment.start, }) { if (icon == null) { return FilledButton( @@ -125,6 +130,7 @@ class FilledButton extends ButtonStyleButton { statesController: statesController, icon: icon, label: label, + iconAlignment: iconAlignment, ); } @@ -167,6 +173,7 @@ class FilledButton extends ButtonStyleButton { MaterialStatesController? statesController, Widget? icon, required Widget label, + IconAlignment iconAlignment = IconAlignment.start, }) { if (icon == null) { return FilledButton.tonal( @@ -196,6 +203,7 @@ class FilledButton extends ButtonStyleButton { statesController: statesController, icon: icon, label: label, + iconAlignment: iconAlignment, ); } @@ -535,9 +543,15 @@ class _FilledButtonWithIcon extends FilledButton { super.statesController, required Widget icon, required Widget label, + super.iconAlignment, }) : super( autofocus: autofocus ?? false, - child: _FilledButtonWithIconChild(icon: icon, label: label, buttonStyle: style) + child: _FilledButtonWithIconChild( + icon: icon, + label: label, + buttonStyle: style, + iconAlignment: iconAlignment, + ), ); _FilledButtonWithIcon.tonal({ @@ -553,9 +567,15 @@ class _FilledButtonWithIcon extends FilledButton { super.statesController, required Widget icon, required Widget label, + required IconAlignment iconAlignment, }) : super.tonal( autofocus: autofocus ?? false, - child: _FilledButtonWithIconChild(icon: icon, label: label, buttonStyle: style) + child: _FilledButtonWithIconChild( + icon: icon, + label: label, + buttonStyle: style, + iconAlignment: iconAlignment, + ), ); @override @@ -584,11 +604,17 @@ class _FilledButtonWithIcon extends FilledButton { } class _FilledButtonWithIconChild extends StatelessWidget { - const _FilledButtonWithIconChild({ required this.label, required this.icon, required this.buttonStyle }); + const _FilledButtonWithIconChild({ + required this.label, + required this.icon, + required this.buttonStyle, + required this.iconAlignment, + }); final Widget label; final Widget icon; final ButtonStyle? buttonStyle; + final IconAlignment iconAlignment; @override Widget build(BuildContext context) { @@ -599,7 +625,9 @@ class _FilledButtonWithIconChild extends StatelessWidget { final double gap = lerpDouble(8, 4, scale)!; return Row( mainAxisSize: MainAxisSize.min, - children: [icon, SizedBox(width: gap), Flexible(child: label)], + children: iconAlignment == IconAlignment.start + ? [icon, SizedBox(width: gap), Flexible(child: label)] + : [Flexible(child: label), SizedBox(width: gap), icon], ); } } diff --git a/packages/flutter/lib/src/material/outlined_button.dart b/packages/flutter/lib/src/material/outlined_button.dart index 9109df01b1d..3ca3b7caf38 100644 --- a/packages/flutter/lib/src/material/outlined_button.dart +++ b/packages/flutter/lib/src/material/outlined_button.dart @@ -78,6 +78,7 @@ class OutlinedButton extends ButtonStyleButton { super.clipBehavior, super.statesController, required super.child, + super.iconAlignment, }); /// Create a text button from a pair of widgets that serve as the button's @@ -87,7 +88,10 @@ class OutlinedButton extends ButtonStyleButton { /// at the start, and 16 at the end, with an 8 pixel gap in between. /// /// If [icon] is null, will create an [OutlinedButton] instead. - factory OutlinedButton.icon({ + /// + /// {@macro flutter.material.ButtonStyleButton.iconAlignment} + /// + factory OutlinedButton.icon({ Key? key, required VoidCallback? onPressed, VoidCallback? onLongPress, @@ -98,6 +102,7 @@ class OutlinedButton extends ButtonStyleButton { MaterialStatesController? statesController, Widget? icon, required Widget label, + IconAlignment iconAlignment = IconAlignment.start, }) { if (icon == null) { return OutlinedButton( @@ -123,6 +128,7 @@ class OutlinedButton extends ButtonStyleButton { statesController: statesController, icon: icon, label: label, + iconAlignment: iconAlignment, ); } @@ -455,9 +461,15 @@ class _OutlinedButtonWithIcon extends OutlinedButton { super.statesController, required Widget icon, required Widget label, + super.iconAlignment, }) : super( autofocus: autofocus ?? false, - child: _OutlinedButtonWithIconChild(icon: icon, label: label, buttonStyle: style), + child: _OutlinedButtonWithIconChild( + icon: icon, + label: label, + buttonStyle: style, + iconAlignment: iconAlignment, + ), ); @override @@ -486,11 +498,13 @@ class _OutlinedButtonWithIconChild extends StatelessWidget { required this.label, required this.icon, required this.buttonStyle, + required this.iconAlignment, }); final Widget label; final Widget icon; final ButtonStyle? buttonStyle; + final IconAlignment iconAlignment; @override Widget build(BuildContext context) { @@ -499,7 +513,9 @@ class _OutlinedButtonWithIconChild extends StatelessWidget { final double gap = lerpDouble(8, 4, scale)!; return Row( mainAxisSize: MainAxisSize.min, - children: [icon, SizedBox(width: gap), Flexible(child: label)], + children: iconAlignment == IconAlignment.start + ? [icon, SizedBox(width: gap), Flexible(child: label)] + : [Flexible(child: label), SizedBox(width: gap), icon], ); } } diff --git a/packages/flutter/lib/src/material/text_button.dart b/packages/flutter/lib/src/material/text_button.dart index 769a7d4c719..669217aeec2 100644 --- a/packages/flutter/lib/src/material/text_button.dart +++ b/packages/flutter/lib/src/material/text_button.dart @@ -87,6 +87,7 @@ class TextButton extends ButtonStyleButton { super.statesController, super.isSemanticButton, required Widget super.child, + super.iconAlignment, }); /// Create a text button from a pair of widgets that serve as the button's @@ -94,6 +95,11 @@ class TextButton extends ButtonStyleButton { /// /// The icon and label are arranged in a row and padded by 8 logical pixels /// at the ends, with an 8 pixel gap in between. + /// + /// If [icon] is null, will create a [TextButton] instead. + /// + /// {@macro flutter.material.ButtonStyleButton.iconAlignment} + /// factory TextButton.icon({ Key? key, required VoidCallback? onPressed, @@ -107,6 +113,7 @@ class TextButton extends ButtonStyleButton { MaterialStatesController? statesController, Widget? icon, required Widget label, + IconAlignment iconAlignment = IconAlignment.start, }) { if (icon == null) { return TextButton( @@ -135,6 +142,7 @@ class TextButton extends ButtonStyleButton { statesController: statesController, icon: icon, label: label, + iconAlignment: iconAlignment, ); } @@ -497,9 +505,15 @@ class _TextButtonWithIcon extends TextButton { super.statesController, required Widget icon, required Widget label, + super.iconAlignment, }) : super( autofocus: autofocus ?? false, - child: _TextButtonWithIconChild(icon: icon, label: label, buttonStyle: style), + child: _TextButtonWithIconChild( + icon: icon, + label: label, + buttonStyle: style, + iconAlignment: iconAlignment, + ), ); @override @@ -525,11 +539,13 @@ class _TextButtonWithIconChild extends StatelessWidget { required this.label, required this.icon, required this.buttonStyle, + required this.iconAlignment, }); final Widget label; final Widget icon; final ButtonStyle? buttonStyle; + final IconAlignment iconAlignment; @override Widget build(BuildContext context) { @@ -538,7 +554,9 @@ class _TextButtonWithIconChild extends StatelessWidget { final double gap = lerpDouble(8, 4, scale)!; return Row( mainAxisSize: MainAxisSize.min, - children: [icon, SizedBox(width: gap), Flexible(child: label)], + children: iconAlignment == IconAlignment.start + ? [icon, SizedBox(width: gap), Flexible(child: label)] + : [Flexible(child: label), SizedBox(width: gap), icon], ); } } diff --git a/packages/flutter/test/material/elevated_button_test.dart b/packages/flutter/test/material/elevated_button_test.dart index e7f1a6e68c9..f3042f50030 100644 --- a/packages/flutter/test/material/elevated_button_test.dart +++ b/packages/flutter/test/material/elevated_button_test.dart @@ -2178,6 +2178,118 @@ void main() { focusNode.dispose(); }); + + testWidgets('Default iconAlignment', (WidgetTester tester) async { + Widget buildWidget({ required TextDirection textDirection }) { + return MaterialApp( + home: Directionality( + textDirection: textDirection, + child: Center( + child: ElevatedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + ), + ), + ), + ); + } + + // Test default iconAlignment when textDirection is ltr. + await tester.pumpWidget(buildWidget(textDirection: TextDirection.ltr)); + + final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + // The icon is aligned to the left of the button. + expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); // 16.0 - padding between icon and button edge. + + // Test default iconAlignment when textDirection is rtl. + await tester.pumpWidget(buildWidget(textDirection: TextDirection.rtl)); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + // The icon is aligned to the right of the button. + expect(buttonTopRight.dx, iconTopRight.dx + 16.0); // 16.0 - padding between icon and button edge. + }); + + testWidgets('iconAlignment can be customized', (WidgetTester tester) async { + Widget buildWidget({ + required TextDirection textDirection, + required IconAlignment iconAlignment, + }) { + return MaterialApp( + home: Directionality( + textDirection: textDirection, + child: Center( + child: ElevatedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + iconAlignment: iconAlignment, + ), + ), + ), + ); + } + + // Test iconAlignment when textDirection is ltr. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.ltr, + iconAlignment: IconAlignment.start, + ), + ); + + Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + // The icon is aligned to the left of the button. + expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); // 16.0 - padding between icon and button edge. + + // Test iconAlignment when textDirection is ltr. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.ltr, + iconAlignment: IconAlignment.end, + ), + ); + + Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + // The icon is aligned to the right of the button. + expect(buttonTopRight.dx, iconTopRight.dx + 24.0); // 24.0 - padding between icon and button edge. + + // Test iconAlignment when textDirection is rtl. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.rtl, + iconAlignment: IconAlignment.start, + ), + ); + + buttonTopRight = tester.getTopRight(find.byType(Material).last); + iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + // The icon is aligned to the right of the button. + expect(buttonTopRight.dx, iconTopRight.dx + 16.0); // 16.0 - padding between icon and button edge. + + // Test iconAlignment when textDirection is rtl. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.rtl, + iconAlignment: IconAlignment.end, + ), + ); + + buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + // The icon is aligned to the left of the button. + expect(buttonTopLeft.dx, iconTopLeft.dx - 24.0); // 24.0 - padding between icon and button edge. + }); } TextStyle _iconStyle(WidgetTester tester, IconData icon) { diff --git a/packages/flutter/test/material/filled_button_test.dart b/packages/flutter/test/material/filled_button_test.dart index 535d8ba52f8..0827808d04f 100644 --- a/packages/flutter/test/material/filled_button_test.dart +++ b/packages/flutter/test/material/filled_button_test.dart @@ -2149,7 +2149,6 @@ void main() { expect(textChildOf(decorations.at(1)).data, 'button'); }); - testWidgets('FilledButton backgroundBuilder drops button child and foregroundBuilder return value', (WidgetTester tester) async { const Color backgroundColor = Color(0xFF000011); const Color foregroundColor = Color(0xFF000022); @@ -2288,6 +2287,230 @@ void main() { focusNode.dispose(); }); + + testWidgets('Default iconAlignment', (WidgetTester tester) async { + Widget buildWidget({ required TextDirection textDirection }) { + return MaterialApp( + home: Directionality( + textDirection: textDirection, + child: Center( + child: FilledButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + ), + ), + ), + ); + } + + // Test default iconAlignment when textDirection is ltr. + await tester.pumpWidget(buildWidget(textDirection: TextDirection.ltr)); + + final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + // The icon is aligned to the left of the button. + expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); // 16.0 - padding between icon and button edge. + + // Test default iconAlignment when textDirection is rtl. + await tester.pumpWidget(buildWidget(textDirection: TextDirection.rtl)); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + // The icon is aligned to the right of the button. + expect(buttonTopRight.dx, iconTopRight.dx + 16.0); // 16.0 - padding between icon and button edge. + }); + + testWidgets('iconAlignment can be customized', (WidgetTester tester) async { + Widget buildWidget({ + required TextDirection textDirection, + required IconAlignment iconAlignment, + }) { + return MaterialApp( + home: Directionality( + textDirection: textDirection, + child: Center( + child: FilledButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + iconAlignment: iconAlignment, + ), + ), + ), + ); + } + + // Test iconAlignment when textDirection is ltr. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.ltr, + iconAlignment: IconAlignment.start, + ), + ); + + Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + // The icon is aligned to the left of the button. + expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); // 16.0 - padding between icon and button edge. + + // Test iconAlignment when textDirection is ltr. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.ltr, + iconAlignment: IconAlignment.end, + ), + ); + + Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + // The icon is aligned to the right of the button. + expect(buttonTopRight.dx, iconTopRight.dx + 24.0); // 24.0 - padding between icon and button edge. + + // Test iconAlignment when textDirection is rtl. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.rtl, + iconAlignment: IconAlignment.start, + ), + ); + + buttonTopRight = tester.getTopRight(find.byType(Material).last); + iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + // The icon is aligned to the right of the button. + expect(buttonTopRight.dx, iconTopRight.dx + 16.0); // 16.0 - padding between icon and button edge. + + // Test iconAlignment when textDirection is rtl. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.rtl, + iconAlignment: IconAlignment.end, + ), + ); + + buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + // The icon is aligned to the left of the button. + expect(buttonTopLeft.dx, iconTopLeft.dx - 24.0); // 24.0 - padding between icon and button edge. + }); + + testWidgets('Tonal icon default iconAlignment', (WidgetTester tester) async { + Widget buildWidget({ required TextDirection textDirection }) { + return MaterialApp( + home: Directionality( + textDirection: textDirection, + child: Center( + child: FilledButton.tonalIcon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + ), + ), + ), + ); + } + + // Test default iconAlignment when textDirection is ltr. + await tester.pumpWidget(buildWidget(textDirection: TextDirection.ltr)); + + final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + // The icon is aligned to the left of the button. + expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); // 16.0 - padding between icon and button edge. + + // Test default iconAlignment when textDirection is rtl. + await tester.pumpWidget(buildWidget(textDirection: TextDirection.rtl)); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + // The icon is aligned to the right of the button. + expect(buttonTopRight.dx, iconTopRight.dx + 16.0); // 16.0 - padding between icon and button edge. + }); + + testWidgets('Tonal icon iconAlignment can be customized', (WidgetTester tester) async { + Widget buildWidget({ + required TextDirection textDirection, + required IconAlignment iconAlignment, + }) { + return MaterialApp( + home: Directionality( + textDirection: textDirection, + child: Center( + child: FilledButton.tonalIcon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + iconAlignment: iconAlignment, + ), + ), + ), + ); + } + + // Test iconAlignment when textDirection is ltr. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.ltr, + iconAlignment: IconAlignment.start, + ), + ); + + Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + // The icon is aligned to the left of the button. + expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); // 16.0 - padding between icon and button edge. + + // Test iconAlignment when textDirection is ltr. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.ltr, + iconAlignment: IconAlignment.end, + ), + ); + + Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + // The icon is aligned to the right of the button. + expect(buttonTopRight.dx, iconTopRight.dx + 24.0); // 24.0 - padding between icon and button edge. + + // Test iconAlignment when textDirection is rtl. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.rtl, + iconAlignment: IconAlignment.start, + ), + ); + + buttonTopRight = tester.getTopRight(find.byType(Material).last); + iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + // The icon is aligned to the right of the button. + expect(buttonTopRight.dx, iconTopRight.dx + 16.0); // 16.0 - padding between icon and button edge. + + // Test iconAlignment when textDirection is rtl. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.rtl, + iconAlignment: IconAlignment.end, + ), + ); + + buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + // The icon is aligned to the left of the button. + expect(buttonTopLeft.dx, iconTopLeft.dx - 24.0); // 24.0 - padding between icon and button edge. + }); } TextStyle _iconStyle(WidgetTester tester, IconData icon) { diff --git a/packages/flutter/test/material/outlined_button_test.dart b/packages/flutter/test/material/outlined_button_test.dart index 6da90432869..3bef05df9f0 100644 --- a/packages/flutter/test/material/outlined_button_test.dart +++ b/packages/flutter/test/material/outlined_button_test.dart @@ -2189,7 +2189,6 @@ void main() { expect(textChildOf(decorations.at(1)).data, 'button'); }); - testWidgets('OutlinedButton backgroundBuilder drops button child and foregroundBuilder return value', (WidgetTester tester) async { const Color backgroundColor = Color(0xFF000011); const Color foregroundColor = Color(0xFF000022); @@ -2318,7 +2317,6 @@ void main() { expect(sameStates(focusedHoveredStates, backgroundStates), isTrue); expect(sameStates(focusedHoveredStates, foregroundStates), isTrue); - // Highlighted (pressed). await gesture.down(center); await tester.pump(); // Start the splash and highlight animations. @@ -2357,6 +2355,118 @@ void main() { await tester.pumpWidget(buildFrame()); // onPressed: null - disabled expect(material.color, backgroundColor); }); + + testWidgets('Default iconAlignment', (WidgetTester tester) async { + Widget buildWidget({ required TextDirection textDirection }) { + return MaterialApp( + home: Directionality( + textDirection: textDirection, + child: Center( + child: OutlinedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + ), + ), + ), + ); + } + + // Test default iconAlignment when textDirection is ltr. + await tester.pumpWidget(buildWidget(textDirection: TextDirection.ltr)); + + final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + // The icon is aligned to the left of the button. + expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); // 16.0 - padding between icon and button edge. + + // Test default iconAlignment when textDirection is rtl. + await tester.pumpWidget(buildWidget(textDirection: TextDirection.rtl)); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + // The icon is aligned to the right of the button. + expect(buttonTopRight.dx, iconTopRight.dx + 16.0); // 16.0 - padding between icon and button edge. + }); + + testWidgets('iconAlignment can be customized', (WidgetTester tester) async { + Widget buildWidget({ + required TextDirection textDirection, + required IconAlignment iconAlignment, + }) { + return MaterialApp( + home: Directionality( + textDirection: textDirection, + child: Center( + child: OutlinedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + iconAlignment: iconAlignment, + ), + ), + ), + ); + } + + // Test iconAlignment when textDirection is ltr. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.ltr, + iconAlignment: IconAlignment.start, + ), + ); + + Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + // The icon is aligned to the left of the button. + expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); // 16.0 - padding between icon and button edge. + + // Test iconAlignment when textDirection is ltr. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.ltr, + iconAlignment: IconAlignment.end, + ), + ); + + Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + // The icon is aligned to the right of the button. + expect(buttonTopRight.dx, iconTopRight.dx + 24.0); // 24.0 - padding between icon and button edge. + + // Test iconAlignment when textDirection is rtl. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.rtl, + iconAlignment: IconAlignment.start, + ), + ); + + buttonTopRight = tester.getTopRight(find.byType(Material).last); + iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + // The icon is aligned to the right of the button. + expect(buttonTopRight.dx, iconTopRight.dx + 16.0); // 16.0 - padding between icon and button edge. + + // Test iconAlignment when textDirection is rtl. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.rtl, + iconAlignment: IconAlignment.end, + ), + ); + + buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + // The icon is aligned to the left of the button. + expect(buttonTopLeft.dx, iconTopLeft.dx - 24.0); // 24.0 - padding between icon and button edge. + }); } TextStyle _iconStyle(WidgetTester tester, IconData icon) { diff --git a/packages/flutter/test/material/text_button_test.dart b/packages/flutter/test/material/text_button_test.dart index 7a63d4c16eb..6d7e3e84212 100644 --- a/packages/flutter/test/material/text_button_test.dart +++ b/packages/flutter/test/material/text_button_test.dart @@ -2022,7 +2022,6 @@ void main() { expect(textChildOf(decorations.at(1)).data, 'button'); }); - testWidgets('TextButton backgroundBuilder drops button child and foregroundBuilder return value', (WidgetTester tester) async { const Color backgroundColor = Color(0xFF000011); const Color foregroundColor = Color(0xFF000022); @@ -2151,7 +2150,6 @@ void main() { expect(sameStates(focusedHoveredStates, backgroundStates), isTrue); expect(sameStates(focusedHoveredStates, foregroundStates), isTrue); - // Highlighted (pressed). await gesture.down(center); await tester.pump(); // Start the splash and highlight animations. @@ -2190,6 +2188,118 @@ void main() { await tester.pumpWidget(buildFrame()); // onPressed: null - disabled expect(material.color, backgroundColor); }); + + testWidgets('Default iconAlignment', (WidgetTester tester) async { + Widget buildWidget({ required TextDirection textDirection }) { + return MaterialApp( + home: Directionality( + textDirection: textDirection, + child: Center( + child: TextButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + ), + ), + ), + ); + } + + // Test default iconAlignment when textDirection is ltr. + await tester.pumpWidget(buildWidget(textDirection: TextDirection.ltr)); + + final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + // The icon is aligned to the left of the button. + expect(buttonTopLeft.dx, iconTopLeft.dx - 12.0); // 12.0 - padding between icon and button edge. + + // Test default iconAlignment when textDirection is rtl. + await tester.pumpWidget(buildWidget(textDirection: TextDirection.rtl)); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + // The icon is aligned to the right of the button. + expect(buttonTopRight.dx, iconTopRight.dx + 12.0); // 12.0 - padding between icon and button edge. + }); + + testWidgets('iconAlignment can be customized', (WidgetTester tester) async { + Widget buildWidget({ + required TextDirection textDirection, + required IconAlignment iconAlignment, + }) { + return MaterialApp( + home: Directionality( + textDirection: textDirection, + child: Center( + child: TextButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + iconAlignment: iconAlignment, + ), + ), + ), + ); + } + + // Test iconAlignment when textDirection is ltr. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.ltr, + iconAlignment: IconAlignment.start, + ), + ); + + Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + // The icon is aligned to the left of the button. + expect(buttonTopLeft.dx, iconTopLeft.dx - 12.0); // 12.0 - padding between icon and button edge. + + // Test iconAlignment when textDirection is ltr. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.ltr, + iconAlignment: IconAlignment.end, + ), + ); + + Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + // The icon is aligned to the right of the button. + expect(buttonTopRight.dx, iconTopRight.dx + 16.0); // 16.0 - padding between icon and button edge. + + // Test iconAlignment when textDirection is rtl. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.rtl, + iconAlignment: IconAlignment.start, + ), + ); + + buttonTopRight = tester.getTopRight(find.byType(Material).last); + iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + // The icon is aligned to the right of the button. + expect(buttonTopRight.dx, iconTopRight.dx + 12.0); // 12.0 - padding between icon and button edge. + + // Test iconAlignment when textDirection is rtl. + await tester.pumpWidget( + buildWidget( + textDirection: TextDirection.rtl, + iconAlignment: IconAlignment.end, + ), + ); + + buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + // The icon is aligned to the left of the button. + expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); // 16.0 - padding between icon and button edge. + }); } TextStyle? _iconStyle(WidgetTester tester, IconData icon) {