From 9dce19e96ff36fc36c50d15f57fa20e096a5b29f Mon Sep 17 00:00:00 2001 From: Darren Austin Date: Mon, 19 Aug 2019 17:35:44 -0700 Subject: [PATCH] Replace ButtonBar.bar method with ButtonBarTheme (#37544) * Added new ButtonBarTheme to replace the deprecated ButtonTheme.bar method. * Responding to PR feedback. * [Material] Create material Banner component (#36880) This PR creates a new material widget for the Banner component. This includes a theme as well. This widget can be dropped into any application, ideally at the top of a listview or scrollview. (cherry picked from commit 35b6d668e197035ea2b57adb99041542982e8be0) Removed the use of ButtonTheme.bar in the Banner implementation. * Updated documentation from PR review comments. --- .../lib/demo/material/cards_demo.dart | 30 +- packages/flutter/lib/material.dart | 1 + packages/flutter/lib/src/material/banner.dart | 6 +- .../flutter/lib/src/material/button_bar.dart | 129 +++++-- .../lib/src/material/button_bar_theme.dart | 240 +++++++++++++ .../lib/src/material/button_theme.dart | 49 ++- packages/flutter/lib/src/material/card.dart | 27 +- .../flutter/lib/src/material/date_picker.dart | 25 +- packages/flutter/lib/src/material/dialog.dart | 7 +- .../src/material/paginated_data_table.dart | 19 +- .../flutter/lib/src/material/scaffold.dart | 11 +- .../flutter/lib/src/material/snack_bar.dart | 5 +- .../flutter/lib/src/material/theme_data.dart | 19 +- .../flutter/lib/src/material/time_picker.dart | 25 +- .../test/material/button_bar_test.dart | 333 ++++++++++++++---- .../test/material/button_bar_theme_test.dart | 150 ++++++++ 16 files changed, 877 insertions(+), 199 deletions(-) create mode 100644 packages/flutter/lib/src/material/button_bar_theme.dart create mode 100644 packages/flutter/test/material/button_bar_theme_test.dart diff --git a/examples/flutter_gallery/lib/demo/material/cards_demo.dart b/examples/flutter_gallery/lib/demo/material/cards_demo.dart index 4e3b7880657..39c110695a1 100644 --- a/examples/flutter_gallery/lib/demo/material/cards_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/cards_demo.dart @@ -324,22 +324,20 @@ class TravelDestinationContent extends StatelessWidget { if (destination.type == CardDemoType.standard) { children.add( // share, explore buttons - ButtonTheme.bar( - child: ButtonBar( - alignment: MainAxisAlignment.start, - children: [ - FlatButton( - child: Text('SHARE', semanticsLabel: 'Share ${destination.title}'), - textColor: Colors.amber.shade500, - onPressed: () { print('pressed'); }, - ), - FlatButton( - child: Text('EXPLORE', semanticsLabel: 'Explore ${destination.title}'), - textColor: Colors.amber.shade500, - onPressed: () { print('pressed'); }, - ), - ], - ), + ButtonBar( + alignment: MainAxisAlignment.start, + children: [ + FlatButton( + child: Text('SHARE', semanticsLabel: 'Share ${destination.title}'), + textColor: Colors.amber.shade500, + onPressed: () { print('pressed'); }, + ), + FlatButton( + child: Text('EXPLORE', semanticsLabel: 'Explore ${destination.title}'), + textColor: Colors.amber.shade500, + onPressed: () { print('pressed'); }, + ), + ], ), ); } diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index e951962dc09..076af21773f 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -32,6 +32,7 @@ export 'src/material/bottom_sheet.dart'; export 'src/material/bottom_sheet_theme.dart'; export 'src/material/button.dart'; export 'src/material/button_bar.dart'; +export 'src/material/button_bar_theme.dart'; export 'src/material/button_theme.dart'; export 'src/material/card.dart'; export 'src/material/card_theme.dart'; diff --git a/packages/flutter/lib/src/material/banner.dart b/packages/flutter/lib/src/material/banner.dart index bac6fd968f5..b3364c9feac 100644 --- a/packages/flutter/lib/src/material/banner.dart +++ b/packages/flutter/lib/src/material/banner.dart @@ -118,11 +118,9 @@ class MaterialBanner extends StatelessWidget { ?? bannerTheme.padding ?? const EdgeInsetsDirectional.only(end: 16.0); - final Widget buttonBar = ButtonTheme.bar( + final Widget buttonBar = ButtonBar( layoutBehavior: ButtonBarLayoutBehavior.constrained, - child: ButtonBar( - children: actions, - ), + children: actions, ); final Color backgroundColor = this.backgroundColor diff --git a/packages/flutter/lib/src/material/button_bar.dart b/packages/flutter/lib/src/material/button_bar.dart index 2e4ed41021d..4e5298c6f3d 100644 --- a/packages/flutter/lib/src/material/button_bar.dart +++ b/packages/flutter/lib/src/material/button_bar.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; +import 'button_bar_theme.dart'; import 'button_theme.dart'; import 'dialog.dart'; import 'flat_button.dart'; @@ -11,12 +12,24 @@ import 'raised_button.dart'; /// An end-aligned row of buttons. /// -/// Places the buttons horizontally according to the padding in the current -/// [ButtonTheme]. The children are laid out in a [Row] with -/// [MainAxisAlignment.end]. When the [Directionality] is [TextDirection.ltr], -/// the button bar's children are right justified and the last child becomes -/// the rightmost child. When the [Directionality] [TextDirection.rtl] the -/// children are left justified and the last child becomes the leftmost child. +/// Places the buttons horizontally according to the [buttonPadding]. The +/// children are laid out in a [Row] with [MainAxisAlignment.end]. When the +/// [Directionality] is [TextDirection.ltr], the button bar's children are +/// right justified and the last child becomes the rightmost child. When the +/// [Directionality] [TextDirection.rtl] the children are left justified and +/// the last child becomes the leftmost child. +/// +/// The [ButtonBar] can be configured with a [ButtonBarTheme]. For any null +/// property on the ButtonBar, the surrounding ButtonBarTheme's property +/// will be used instead. If the ButtonBarTheme's property is null +/// as well, the property will default to a value described in the field +/// documentation below. +/// +/// The [children] are wrapped in a [ButtonTheme] that is a copy of the +/// surrounding ButtonTheme with the button properties overridden by the +/// properties of the ButtonBar as described above. These properties include +/// [buttonTextTheme], [buttonMinWidth], [buttonHeight], [buttonPadding], +/// and [buttonAlignedDropdown]. /// /// Used by [Dialog] to arrange the actions at the bottom of the dialog. /// @@ -26,24 +39,84 @@ import 'raised_button.dart'; /// * [FlatButton], another kind of button. /// * [Card], at the bottom of which it is common to place a [ButtonBar]. /// * [Dialog], which uses a [ButtonBar] for its actions. -/// * [ButtonTheme], which configures the [ButtonBar]. +/// * [ButtonBarTheme], which configures the [ButtonBar]. class ButtonBar extends StatelessWidget { /// Creates a button bar. /// - /// The alignment argument defaults to [MainAxisAlignment.end]. + /// Both [buttonMinWidth] and [buttonHeight] must be non-negative if they + /// are not null. const ButtonBar({ Key key, - this.alignment = MainAxisAlignment.end, - this.mainAxisSize = MainAxisSize.max, + this.alignment, + this.mainAxisSize, + this.buttonTextTheme, + this.buttonMinWidth, + this.buttonHeight, + this.buttonPadding, + this.buttonAlignedDropdown, + this.layoutBehavior, this.children = const [], - }) : super(key: key); + }) : assert(buttonMinWidth == null || buttonMinWidth >= 0.0), + assert(buttonHeight == null || buttonHeight >= 0.0), + super(key: key); /// How the children should be placed along the horizontal axis. + /// + /// If null then it will use [ButtonBarTheme.alignment]. If that is null, + /// it will default to [MainAxisAlignment.end]. final MainAxisAlignment alignment; /// How much horizontal space is available. See [Row.mainAxisSize]. + /// + /// If null then it will use the surrounding [ButtonBarTheme.mainAxisSize]. + /// If that is null, it will default to [MainAxisSize.max]. final MainAxisSize mainAxisSize; + /// Overrides the surrounding [ButtonTheme.textTheme] to define a button's + /// base colors, size, internal padding and shape. + /// + /// If null then it will use the surrounding [ButtonBarTheme.buttonTextTheme]. + /// If that is null, it will default to [ButtonTextTheme.primary]. + final ButtonTextTheme buttonTextTheme; + + /// Overrides the surrounding [ButtonThemeData.minWidth] to define a button's + /// minimum width. + /// + /// If null then it will use the surrounding [ButtonBarTheme.buttonMinWidth]. + /// If that is null, it will default to 64.0 logical pixels. + final double buttonMinWidth; + + /// Overrides the surrounding [ButtonThemeData.height] to define a button's + /// minimum height. + /// + /// If null then it will use the surrounding [ButtonBarTheme.buttonHeight]. + /// If that is null, it will default to 36.0 logical pixels. + final double buttonHeight; + + /// Overrides the surrounding [ButtonThemeData.padding] to define the padding + /// for a button's child (typically the button's label). + /// + /// If null then it will use the surrounding [ButtonBarTheme.buttonPadding]. + /// If that is null, it will default to 8.0 logical pixels on the left + /// and right. + final EdgeInsetsGeometry buttonPadding; + + /// Overrides the surrounding [ButtonThemeData.alignedDropdown] to define whether + /// a [DropdownButton] menu's width will match the button's width. + /// + /// If null then it will use the surrounding [ButtonBarTheme.buttonAlignedDropdown]. + /// If that is null, it will default to false. + final bool buttonAlignedDropdown; + + /// Defines whether a [ButtonBar] should size itself with a minimum size + /// constraint or with padding. + /// + /// Overrides the surrounding [ButtonThemeData.layoutBehavior]. + /// + /// If null then it will use the surrounding [ButtonBarTheme.layoutBehavior]. + /// If that is null, it will default [ButtonBarLayoutBehavior.padded]. + final ButtonBarLayoutBehavior layoutBehavior; + /// The buttons to arrange horizontally. /// /// Typically [RaisedButton] or [FlatButton] widgets. @@ -51,18 +124,32 @@ class ButtonBar extends StatelessWidget { @override Widget build(BuildContext context) { - final ButtonThemeData buttonTheme = ButtonTheme.of(context); + final ButtonThemeData parentButtonTheme = ButtonTheme.of(context); + final ButtonBarThemeData barTheme = ButtonBarTheme.of(context); + + final ButtonThemeData buttonTheme = parentButtonTheme.copyWith( + textTheme: buttonTextTheme ?? barTheme.buttonTextTheme ?? ButtonTextTheme.primary, + minWidth: buttonMinWidth ?? barTheme.buttonMinWidth ?? 64.0, + height: buttonHeight ?? barTheme.buttonHeight ?? 36.0, + padding: buttonPadding ?? barTheme.buttonPadding ?? const EdgeInsets.symmetric(horizontal: 8.0), + alignedDropdown: buttonAlignedDropdown ?? barTheme.buttonAlignedDropdown ?? false, + layoutBehavior: layoutBehavior ?? barTheme.layoutBehavior ?? ButtonBarLayoutBehavior.padded, + ); + // We divide by 4.0 because we want half of the average of the left and right padding. final double paddingUnit = buttonTheme.padding.horizontal / 4.0; - final Widget child = Row( - mainAxisAlignment: alignment, - mainAxisSize: mainAxisSize, - children: children.map((Widget child) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: paddingUnit), - child: child, - ); - }).toList(), + final Widget child = ButtonTheme.fromButtonThemeData( + data: buttonTheme, + child: Row( + mainAxisAlignment: alignment ?? barTheme.alignment ?? MainAxisAlignment.end, + mainAxisSize: mainAxisSize ?? barTheme.mainAxisSize ?? MainAxisSize.max, + children: children.map((Widget child) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: paddingUnit), + child: child, + ); + }).toList(), + ), ); switch (buttonTheme.layoutBehavior) { case ButtonBarLayoutBehavior.padded: diff --git a/packages/flutter/lib/src/material/button_bar_theme.dart b/packages/flutter/lib/src/material/button_bar_theme.dart new file mode 100644 index 00000000000..b2bc04a4fce --- /dev/null +++ b/packages/flutter/lib/src/material/button_bar_theme.dart @@ -0,0 +1,240 @@ +// Copyright 2019 The Chromium 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 'dart:ui' show lerpDouble; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import 'button_theme.dart'; +import 'theme.dart'; + +/// Defines the visual properties of [ButtonBar] widgets. +/// +/// Used by [ButtonBarTheme] to control the visual properties of [ButtonBar] +/// instances in a widget subtree. +/// +/// To obtain this configuration, use [ButtonBarTheme.of] to access the closest +/// ancestor [ButtonBarTheme] of the current [BuildContext]. +/// +/// See also: +/// +/// * [ButtonBarTheme], an [InheritedWidget] that propagates the theme down +/// its subtree. +/// * [ButtonBar], which uses this to configure itself and its children +/// button widgets. +class ButtonBarThemeData extends Diagnosticable { + /// Constructs the set of properties used to configure [ButtonBar] widgets. + /// + /// Both [buttonMinWidth] and [buttonHeight] must be non-negative if they + /// are not null. + const ButtonBarThemeData({ + this.alignment, + this.mainAxisSize, + this.buttonTextTheme, + this.buttonMinWidth, + this.buttonHeight, + this.buttonPadding, + this.buttonAlignedDropdown, + this.layoutBehavior, + }) : assert(buttonMinWidth == null || buttonMinWidth >= 0.0), + assert(buttonHeight == null || buttonHeight >= 0.0); + + /// How the children should be placed along the horizontal axis. + final MainAxisAlignment alignment; + + /// How much horizontal space is available. See [Row.mainAxisSize]. + final MainAxisSize mainAxisSize; + + /// Defines a [ButtonBar] button's base colors, and the defaults for + /// the button's minimum size, internal padding, and shape. + /// + /// This will override the surrounding [ButtonTheme.textTheme] setting + /// for buttons contained in the [ButtonBar]. + /// + /// Despite the name, this property is not a [TextTheme], its value is not a + /// collection of [TextStyle]s. + final ButtonTextTheme buttonTextTheme; + + /// The minimum width for [ButtonBar] buttons. + /// + /// This will override the surrounding [ButtonTheme.minWidth] setting + /// for buttons contained in the [ButtonBar]. + /// + /// The actual horizontal space allocated for a button's child is + /// at least this value less the theme's horizontal [padding]. + final double buttonMinWidth; + + /// The minimum height for [ButtonBar] buttons. + /// + /// This will override the surrounding [ButtonTheme.height] setting + /// for buttons contained in the [ButtonBar]. + final double buttonHeight; + + /// Padding for a [ButtonBar] button's child (typically the button's label). + /// + /// This will override the surrounding [ButtonTheme.padding] setting + /// for buttons contained in the [ButtonBar]. + final EdgeInsetsGeometry buttonPadding; + + /// If true, then a [DropdownButton] menu's width will match the [ButtonBar] + /// button's width. + /// + /// If false, then the dropdown's menu will be wider than + /// its button. In either case the dropdown button will line up the leading + /// edge of the menu's value with the leading edge of the values + /// displayed by the menu items. + /// + /// This will override the surrounding [ButtonTheme.alignedDropdown] setting + /// for buttons contained in the [ButtonBar]. + /// + /// This property only affects [DropdownButton] contained in a [ButtonBar] + /// and its menu. + final bool buttonAlignedDropdown; + + /// Defines whether a [ButtonBar] should size itself with a minimum size + /// constraint or with padding. + final ButtonBarLayoutBehavior layoutBehavior; + + /// Creates a copy of this object but with the given fields replaced with the + /// new values. + ButtonBarThemeData copyWith({ + MainAxisAlignment alignment, + MainAxisSize mainAxisSize, + ButtonTextTheme buttonTextTheme, + double buttonMinWidth, + double buttonHeight, + EdgeInsetsGeometry buttonPadding, + bool buttonAlignedDropdown, + ButtonBarLayoutBehavior layoutBehavior, + }) { + return ButtonBarThemeData( + alignment: alignment ?? this.alignment, + mainAxisSize: mainAxisSize ?? this.mainAxisSize, + buttonTextTheme: buttonTextTheme ?? this.buttonTextTheme, + buttonMinWidth: buttonMinWidth ?? this.buttonMinWidth, + buttonHeight: buttonHeight ?? this.buttonHeight, + buttonPadding: buttonPadding ?? this.buttonPadding, + buttonAlignedDropdown: buttonAlignedDropdown ?? this.buttonAlignedDropdown, + layoutBehavior: layoutBehavior ?? this.layoutBehavior, + ); + } + + /// Linearly interpolate between two button bar themes. + /// + /// If both arguments are null, then null is returned. + /// + /// {@macro dart.ui.shadow.lerp} + static ButtonBarThemeData lerp(ButtonBarThemeData a, ButtonBarThemeData b, double t) { + assert(t != null); + if (a == null && b == null) + return null; + return ButtonBarThemeData( + alignment: t < 0.5 ? a.alignment : b.alignment, + mainAxisSize: t < 0.5 ? a.mainAxisSize : b.mainAxisSize, + buttonTextTheme: t < 0.5 ? a.buttonTextTheme : b.buttonTextTheme, + buttonMinWidth: lerpDouble(a?.buttonMinWidth, b?.buttonMinWidth, t), + buttonHeight: lerpDouble(a?.buttonHeight, b?.buttonHeight, t), + buttonPadding: EdgeInsets.lerp(a?.buttonPadding, b?.buttonPadding, t), + buttonAlignedDropdown: t < 0.5 ? a.buttonAlignedDropdown : b.buttonAlignedDropdown, + layoutBehavior: t < 0.5 ? a.layoutBehavior : b.layoutBehavior, + ); + } + + @override + int get hashCode { + return hashValues( + alignment, + mainAxisSize, + buttonTextTheme, + buttonMinWidth, + buttonHeight, + buttonPadding, + buttonAlignedDropdown, + layoutBehavior, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + final ButtonBarThemeData typedOther = other; + return typedOther.alignment == alignment + && typedOther.mainAxisSize == mainAxisSize + && typedOther.buttonTextTheme == buttonTextTheme + && typedOther.buttonMinWidth == buttonMinWidth + && typedOther.buttonHeight == buttonHeight + && typedOther.buttonPadding == buttonPadding + && typedOther.buttonAlignedDropdown == buttonAlignedDropdown + && typedOther.layoutBehavior == layoutBehavior; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('alignment', alignment, defaultValue: null)); + properties.add(DiagnosticsProperty('mainAxisSize', mainAxisSize, defaultValue: null)); + properties.add(DiagnosticsProperty('textTheme', buttonTextTheme, defaultValue: null)); + properties.add(DoubleProperty('minWidth', buttonMinWidth, defaultValue: null)); + properties.add(DoubleProperty('height', buttonHeight, defaultValue: null)); + properties.add(DiagnosticsProperty('padding', buttonPadding, defaultValue: null)); + properties.add(FlagProperty( + 'buttonAlignedDropdown', + value: buttonAlignedDropdown, + ifTrue: 'dropdown width matches button', + defaultValue: null)); + properties.add(DiagnosticsProperty('layoutBehavior', layoutBehavior, defaultValue: null)); + } +} + +/// Applies a button bar theme to descendant [ButtonBar] widgets. +/// +/// A button bar theme describes the layout and properties for the buttons +/// contained in a [ButtonBar]. +/// +/// Descendant widgets obtain the current theme's [ButtonBarTheme] object using +/// [ButtonBarTheme.of]. When a widget uses [ButtonBarTheme.of], it is automatically +/// rebuilt if the theme later changes. +/// +/// A button bar theme can be specified as part of the overall Material theme +/// using [ThemeData.buttonBarTheme]. +/// +/// See also: +/// +/// * [ButtonBarThemeData], which describes the actual configuration of a button +/// bar theme. +class ButtonBarTheme extends InheritedWidget { + /// Constructs a button bar theme that configures all descendent [ButtonBar] + /// widgets. + /// + /// The [data] must not be null. + const ButtonBarTheme({ + Key key, + @required this.data, + Widget child, + }) : assert(data != null), super(key: key, child: child); + + /// The properties used for all descendant [ButtonBar] widgets. + final ButtonBarThemeData data; + + /// Returns the configuration [data] from the closest [ButtonBarTheme] + /// ancestor. If there is no ancestor, it returns [ThemeData.buttonBarTheme]. + /// Applications can assume that the returned value will not be null. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// ButtonBarThemeData theme = ButtonBarTheme.of(context); + /// ``` + static ButtonBarThemeData of(BuildContext context) { + final ButtonBarTheme buttonBarTheme = context.inheritFromWidgetOfExactType(ButtonBarTheme); + return buttonBarTheme?.data ?? Theme.of(context).buttonBarTheme; + } + + @override + bool updateShouldNotify(ButtonBarTheme oldWidget) => data != oldWidget.data; +} diff --git a/packages/flutter/lib/src/material/button_theme.dart b/packages/flutter/lib/src/material/button_theme.dart index 2bd426f4a63..67afdc4ab60 100644 --- a/packages/flutter/lib/src/material/button_theme.dart +++ b/packages/flutter/lib/src/material/button_theme.dart @@ -120,20 +120,51 @@ class ButtonTheme extends InheritedWidget { }) : assert(data != null), super(key: key, child: child); + // TODO(darrenaustin): remove after this deprecation warning has been on + // stable for a couple of releases. + // See https://github.com/flutter/flutter/issues/37333 + // /// Creates a button theme that is appropriate for button bars, as used in /// dialog footers and in the headers of data tables. /// - /// This theme is denser, with a smaller [minWidth] and [padding], than the - /// default theme. Also, this theme uses [ButtonTextTheme.accent] rather than - /// [ButtonTextTheme.normal]. + /// Deprecated. Please use [ButtonBarTheme] instead which offers more + /// flexibility to configure [ButtonBar] widgets. /// - /// For best effect, the label of the button at the edge of the container - /// should have text that ends up wider than 64.0 pixels. This ensures that - /// the alignment of the text matches the alignment of the edge of the - /// container. + /// To migrate instances of code that were just wrapping a [ButtonBar]: /// - /// For example, buttons at the bottom of [Dialog] or [Card] widgets use this - /// button theme. + /// ```dart + /// ButtonTheme.bar( + /// child: ButtonBar(...) + /// ); + /// ``` + /// + /// you can just remove the `ButtonTheme.bar` as the defaults are now handled + /// by [ButtonBar] directly. + /// + /// If you have more complicated usages of `ButtonTheme.bar` like: + /// + /// ```dart + /// ButtonTheme.bar( + /// padding: EdgeInsets.symmetric(horizontal: 10.0), + /// textTheme: ButtonTextTheme.accent, + /// child: ButtonBar(...), + /// ); + /// ``` + /// + /// you can remove the `ButtonTheme.bar` and move the parameters to the + /// [ButtonBar] instance directly: + /// + /// ```dart + /// ButtonBar( + /// padding: EdgeInsets.symmetric(horizontal: 10.0), + /// textTheme: ButtonTextTheme.accent, + /// ... + /// ); + /// ``` + /// + /// You can also replace the defaults for all [ButtonBar] widgets by updating + /// [ThemeData.buttonBarTheme] for your app. + @Deprecated('use ButtonBarTheme instead') ButtonTheme.bar({ Key key, ButtonTextTheme textTheme = ButtonTextTheme.accent, diff --git a/packages/flutter/lib/src/material/card.dart b/packages/flutter/lib/src/material/card.dart index a8c45b66399..1ff6aa04ea2 100644 --- a/packages/flutter/lib/src/material/card.dart +++ b/packages/flutter/lib/src/material/card.dart @@ -36,19 +36,17 @@ import 'theme.dart'; /// title: Text('The Enchanted Nightingale'), /// subtitle: Text('Music by Julie Gable. Lyrics by Sidney Stein.'), /// ), -/// ButtonTheme.bar( // make buttons use the appropriate styles for cards -/// child: ButtonBar( -/// children: [ -/// FlatButton( -/// child: const Text('BUY TICKETS'), -/// onPressed: () { /* ... */ }, -/// ), -/// FlatButton( -/// child: const Text('LISTEN'), -/// onPressed: () { /* ... */ }, -/// ), -/// ], -/// ), +/// ButtonBar( +/// children: [ +/// FlatButton( +/// child: const Text('BUY TICKETS'), +/// onPressed: () { /* ... */ }, +/// ), +/// FlatButton( +/// child: const Text('LISTEN'), +/// onPressed: () { /* ... */ }, +/// ), +/// ], /// ), /// ], /// ), @@ -92,8 +90,7 @@ import 'theme.dart'; /// See also: /// /// * [ListTile], to display icons and text in a card. -/// * [ButtonBar], to display buttons at the bottom of a card. Typically these -/// would be styled using a [ButtonTheme] created with [new ButtonTheme.bar]. +/// * [ButtonBar], to display buttons at the bottom of a card. /// * [showDialog], to display a modal card. /// * class Card extends StatelessWidget { diff --git a/packages/flutter/lib/src/material/date_picker.dart b/packages/flutter/lib/src/material/date_picker.dart index 7e3e985311f..bead9d44f2a 100644 --- a/packages/flutter/lib/src/material/date_picker.dart +++ b/packages/flutter/lib/src/material/date_picker.dart @@ -11,7 +11,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; import 'button_bar.dart'; -import 'button_theme.dart'; import 'colors.dart'; import 'debug.dart'; import 'dialog.dart'; @@ -986,19 +985,17 @@ class _DatePickerDialogState extends State<_DatePickerDialog> { Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final Widget picker = _buildPicker(); - final Widget actions = ButtonTheme.bar( - child: ButtonBar( - children: [ - FlatButton( - child: Text(localizations.cancelButtonLabel), - onPressed: _handleCancel, - ), - FlatButton( - child: Text(localizations.okButtonLabel), - onPressed: _handleOk, - ), - ], - ), + final Widget actions = ButtonBar( + children: [ + FlatButton( + child: Text(localizations.cancelButtonLabel), + onPressed: _handleCancel, + ), + FlatButton( + child: Text(localizations.okButtonLabel), + onPressed: _handleOk, + ), + ], ); final Dialog dialog = Dialog( diff --git a/packages/flutter/lib/src/material/dialog.dart b/packages/flutter/lib/src/material/dialog.dart index 36f0209edcd..167c0abf7ad 100644 --- a/packages/flutter/lib/src/material/dialog.dart +++ b/packages/flutter/lib/src/material/dialog.dart @@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'button_bar.dart'; -import 'button_theme.dart'; import 'colors.dart'; import 'debug.dart'; import 'dialog_theme.dart'; @@ -343,10 +342,8 @@ class AlertDialog extends StatelessWidget { } if (actions != null) { - children.add(ButtonTheme.bar( - child: ButtonBar( - children: actions, - ), + children.add(ButtonBar( + children: actions, )); } diff --git a/packages/flutter/lib/src/material/paginated_data_table.dart b/packages/flutter/lib/src/material/paginated_data_table.dart index 0627791f4d5..34510153828 100644 --- a/packages/flutter/lib/src/material/paginated_data_table.dart +++ b/packages/flutter/lib/src/material/paginated_data_table.dart @@ -9,7 +9,6 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; import 'button_bar.dart'; -import 'button_theme.dart'; import 'card.dart'; import 'constants.dart'; import 'data_table.dart'; @@ -439,16 +438,14 @@ class PaginatedDataTableState extends State { data: const IconThemeData( opacity: 0.54 ), - child: ButtonTheme.bar( - child: Ink( - height: 64.0, - color: _selectedRowCount > 0 ? themeData.secondaryHeaderColor : null, - child: Padding( - padding: EdgeInsetsDirectional.only(start: startPadding, end: 14.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: headerWidgets, - ), + child: Ink( + height: 64.0, + color: _selectedRowCount > 0 ? themeData.secondaryHeaderColor : null, + child: Padding( + padding: EdgeInsetsDirectional.only(start: startPadding, end: 14.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: headerWidgets, ), ), ), diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index d71d76a26e1..4488d8e8ef2 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -14,7 +14,6 @@ import 'package:flutter/gestures.dart' show DragStartBehavior; import 'app_bar.dart'; import 'bottom_sheet.dart'; import 'button_bar.dart'; -import 'button_theme.dart'; import 'colors.dart'; import 'divider.dart'; import 'drawer.dart'; @@ -2154,13 +2153,9 @@ class ScaffoldState extends State with TickerProviderStateMixin { ), ), child: SafeArea( - child: ButtonTheme.bar( - child: SafeArea( - top: false, - child: ButtonBar( - children: widget.persistentFooterButtons, - ), - ), + top: false, + child: ButtonBar( + children: widget.persistentFooterButtons, ), ), ), diff --git a/packages/flutter/lib/src/material/snack_bar.dart b/packages/flutter/lib/src/material/snack_bar.dart index e5e63986ee3..db70eec5180 100644 --- a/packages/flutter/lib/src/material/snack_bar.dart +++ b/packages/flutter/lib/src/material/snack_bar.dart @@ -301,9 +301,10 @@ class SnackBar extends StatelessWidget { ), ]; if (action != null) { - children.add(ButtonTheme.bar( - padding: EdgeInsets.symmetric(horizontal: snackBarPadding), + children.add(ButtonTheme( textTheme: ButtonTextTheme.accent, + minWidth: 64.0, + padding: EdgeInsets.symmetric(horizontal: snackBarPadding), child: action, )); } else { diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 981a3871ff5..dacad329298 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -13,6 +13,7 @@ import 'app_bar_theme.dart'; import 'banner_theme.dart'; import 'bottom_app_bar_theme.dart'; import 'bottom_sheet_theme.dart'; +import 'button_bar_theme.dart'; import 'button_theme.dart'; import 'card_theme.dart'; import 'chip_theme.dart'; @@ -180,6 +181,7 @@ class ThemeData extends Diagnosticable { PopupMenuThemeData popupMenuTheme, MaterialBannerThemeData bannerTheme, DividerThemeData dividerTheme, + ButtonBarThemeData buttonBarTheme, }) { brightness ??= Brightness.light; final bool isDark = brightness == Brightness.dark; @@ -285,6 +287,7 @@ class ThemeData extends Diagnosticable { popupMenuTheme ??= const PopupMenuThemeData(); bannerTheme ??= const MaterialBannerThemeData(); dividerTheme ??= const DividerThemeData(); + buttonBarTheme ??= const ButtonBarThemeData(); return ThemeData.raw( brightness: brightness, @@ -348,6 +351,7 @@ class ThemeData extends Diagnosticable { popupMenuTheme: popupMenuTheme, bannerTheme: bannerTheme, dividerTheme: dividerTheme, + buttonBarTheme: buttonBarTheme, ); } @@ -423,6 +427,7 @@ class ThemeData extends Diagnosticable { @required this.popupMenuTheme, @required this.bannerTheme, @required this.dividerTheme, + @required this.buttonBarTheme, }) : assert(brightness != null), assert(primaryColor != null), assert(primaryColorBrightness != null), @@ -480,7 +485,8 @@ class ThemeData extends Diagnosticable { assert(bottomSheetTheme != null), assert(popupMenuTheme != null), assert(bannerTheme != null), - assert(dividerTheme != null); + assert(dividerTheme != null), + assert(buttonBarTheme != null); /// Create a [ThemeData] based on the colors in the given [colorScheme] and /// text styles of the optional [textTheme]. @@ -877,6 +883,9 @@ class ThemeData extends Diagnosticable { /// [VerticalDivider]s, etc. final DividerThemeData dividerTheme; + /// A theme for customizing the appearance and layout of [ButtonBar] widgets. + final ButtonBarThemeData buttonBarTheme; + /// Creates a copy of this theme but with the given fields replaced with the new values. ThemeData copyWith({ Brightness brightness, @@ -940,6 +949,7 @@ class ThemeData extends Diagnosticable { PopupMenuThemeData popupMenuTheme, MaterialBannerThemeData bannerTheme, DividerThemeData dividerTheme, + ButtonBarThemeData buttonBarTheme, }) { cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault(); return ThemeData.raw( @@ -1004,6 +1014,7 @@ class ThemeData extends Diagnosticable { popupMenuTheme: popupMenuTheme ?? this.popupMenuTheme, bannerTheme: bannerTheme ?? this.bannerTheme, dividerTheme: dividerTheme ?? this.dividerTheme, + buttonBarTheme: buttonBarTheme ?? this.buttonBarTheme, ); } @@ -1146,6 +1157,7 @@ class ThemeData extends Diagnosticable { popupMenuTheme: PopupMenuThemeData.lerp(a.popupMenuTheme, b.popupMenuTheme, t), bannerTheme: MaterialBannerThemeData.lerp(a.bannerTheme, b.bannerTheme, t), dividerTheme: DividerThemeData.lerp(a.dividerTheme, b.dividerTheme, t), + buttonBarTheme: ButtonBarThemeData.lerp(a.buttonBarTheme, b.buttonBarTheme, t), ); } @@ -1215,7 +1227,8 @@ class ThemeData extends Diagnosticable { (otherData.bottomSheetTheme == bottomSheetTheme) && (otherData.popupMenuTheme == popupMenuTheme) && (otherData.bannerTheme == bannerTheme) && - (otherData.dividerTheme == dividerTheme); + (otherData.dividerTheme == dividerTheme) && + (otherData.buttonBarTheme == buttonBarTheme); } @override @@ -1285,6 +1298,7 @@ class ThemeData extends Diagnosticable { popupMenuTheme, bannerTheme, dividerTheme, + buttonBarTheme, ]; return hashList(values); } @@ -1351,6 +1365,7 @@ class ThemeData extends Diagnosticable { properties.add(DiagnosticsProperty('popupMenuTheme', popupMenuTheme, defaultValue: defaultData.popupMenuTheme)); properties.add(DiagnosticsProperty('bannerTheme', bannerTheme, defaultValue: defaultData.bannerTheme)); properties.add(DiagnosticsProperty('dividerTheme', dividerTheme, defaultValue: defaultData.dividerTheme)); + properties.add(DiagnosticsProperty('buttonBarTheme', buttonBarTheme, defaultValue: defaultData.buttonBarTheme)); } } diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart index 8eb322b8ba7..d5cf6740723 100644 --- a/packages/flutter/lib/src/material/time_picker.dart +++ b/packages/flutter/lib/src/material/time_picker.dart @@ -10,7 +10,6 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'button_bar.dart'; -import 'button_theme.dart'; import 'colors.dart'; import 'debug.dart'; import 'dialog.dart'; @@ -1613,19 +1612,17 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { ), ); - final Widget actions = ButtonTheme.bar( - child: ButtonBar( - children: [ - FlatButton( - child: Text(localizations.cancelButtonLabel), - onPressed: _handleCancel, - ), - FlatButton( - child: Text(localizations.okButtonLabel), - onPressed: _handleOk, - ), - ], - ), + final Widget actions = ButtonBar( + children: [ + FlatButton( + child: Text(localizations.cancelButtonLabel), + onPressed: _handleCancel, + ), + FlatButton( + child: Text(localizations.okButtonLabel), + onPressed: _handleOk, + ), + ], ); final Dialog dialog = Dialog( diff --git a/packages/flutter/test/material/button_bar_test.dart b/packages/flutter/test/material/button_bar_test.dart index 2d18937e5e8..fa3c625ea53 100644 --- a/packages/flutter/test/material/button_bar_test.dart +++ b/packages/flutter/test/material/button_bar_test.dart @@ -15,105 +15,282 @@ void main() { ); }); - testWidgets('ButtonBar has a min height of 52 when using ButtonBarLayoutBehavior.constrained', (WidgetTester tester) async { - await tester.pumpWidget( - SingleChildScrollView( - child: ListBody( - children: [ - ButtonTheme.bar( + group('alignment', () { + + testWidgets('default alignment is MainAxisAlignment.end', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: ButtonBar( + children: [ + SizedBox(width: 10.0, height: 10.0), + ], + ), + ) + ); + + final Finder child = find.byType(SizedBox); + // Should be positioned to the right of the bar, + expect(tester.getRect(child).left, 782.0); // bar width - default padding - 10 + expect(tester.getRect(child).right, 792.0); // bar width - default padding + }); + + testWidgets('ButtonBarTheme.alignment overrides default', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: ButtonBarTheme( + data: ButtonBarThemeData( + alignment: MainAxisAlignment.center, + ), + child: ButtonBar( + children: [ + SizedBox(width: 10.0, height: 10.0), + ], + ), + ), + ) + ); + + final Finder child = find.byType(SizedBox); + // Should be positioned in the center + expect(tester.getRect(child).left, 395.0); // (bar width - padding) / 2 - 10 / 2 + expect(tester.getRect(child).right, 405.0); // (bar width - padding) / 2 - 10 / 2 + 10 + }); + + testWidgets('ButtonBar.alignment overrides ButtonBarTheme.alignment and default', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: ButtonBarTheme( + data: ButtonBarThemeData( + alignment: MainAxisAlignment.center, + ), + child: ButtonBar( + alignment: MainAxisAlignment.start, + children: [ + SizedBox(width: 10.0, height: 10.0), + ], + ), + ), + ) + ); + + final Finder child = find.byType(SizedBox); + // Should be positioned on the left + expect(tester.getRect(child).left, 8.0); // padding + expect(tester.getRect(child).right, 18.0); // padding + 10 + }); + + }); + + group('mainAxisSize', () { + + testWidgets('default mainAxisSize is MainAxisSize.max', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ButtonBar( + children: [ + Container(), + ], + ), + ) + ); + + // ButtonBar uses a Row internally to implement this + final Row row = tester.widget(find.byType(Row)); + expect(row.mainAxisSize, equals(MainAxisSize.max)); + }); + + testWidgets('ButtonBarTheme.mainAxisSize overrides default', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ButtonBarTheme( + data: const ButtonBarThemeData( + mainAxisSize: MainAxisSize.min, + ), + child: ButtonBar( + children: [ + Container(), + ], + ), + ), + ) + ); + + // ButtonBar uses a Row internally to implement this + final Row row = tester.widget(find.byType(Row)); + expect(row.mainAxisSize, equals(MainAxisSize.min)); + }); + + testWidgets('ButtonBar.mainAxisSize overrides ButtonBarTheme.mainAxisSize and default', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ButtonBarTheme( + data: const ButtonBarThemeData( + mainAxisSize: MainAxisSize.min, + ), + child: ButtonBar( + mainAxisSize: MainAxisSize.max, + children: [ + Container(), + ], + ), + ), + ) + ); + + // ButtonBar uses a Row internally to implement this + final Row row = tester.widget(find.byType(Row)); + expect(row.mainAxisSize, equals(MainAxisSize.max)); + }); + + }); + + group('button properies override ButtonTheme', () { + + testWidgets('default button properties override ButtonTheme properties', (WidgetTester tester) async { + BuildContext capturedContext; + await tester.pumpWidget( + MaterialApp( + home: ButtonBar( + children: [ + Builder(builder: (BuildContext context) { + capturedContext = context; + return Container(); + }) + ], + ), + ) + ); + final ButtonThemeData buttonTheme = ButtonTheme.of(capturedContext); + expect(buttonTheme.textTheme, equals(ButtonTextTheme.primary)); + expect(buttonTheme.minWidth, equals(64.0)); + expect(buttonTheme.height, equals(36.0)); + expect(buttonTheme.padding, equals(const EdgeInsets.symmetric(horizontal: 8.0))); + expect(buttonTheme.alignedDropdown, equals(false)); + expect(buttonTheme.layoutBehavior, equals(ButtonBarLayoutBehavior.padded)); + }); + + testWidgets('ButtonBarTheme button properties override defaults and ButtonTheme properties', (WidgetTester tester) async { + BuildContext capturedContext; + await tester.pumpWidget( + MaterialApp( + home: ButtonBarTheme( + data: const ButtonBarThemeData( + buttonTextTheme: ButtonTextTheme.primary, + buttonMinWidth: 42.0, + buttonHeight: 84.0, + buttonPadding: EdgeInsets.fromLTRB(10, 20, 30, 40), + buttonAlignedDropdown: true, layoutBehavior: ButtonBarLayoutBehavior.constrained, - child: const Directionality( - textDirection: TextDirection.ltr, - child: ButtonBar( - children: [ - SizedBox(width: 10.0, height: 10.0), - ], - ), - ), ), - ], - ), - ), - ); + child: ButtonBar( + children: [ + Builder(builder: (BuildContext context) { + capturedContext = context; + return Container(); + }) + ], + ), + ), + ) + ); + final ButtonThemeData buttonTheme = ButtonTheme.of(capturedContext); + expect(buttonTheme.textTheme, equals(ButtonTextTheme.primary)); + expect(buttonTheme.minWidth, equals(42.0)); + expect(buttonTheme.height, equals(84.0)); + expect(buttonTheme.padding, equals(const EdgeInsets.fromLTRB(10, 20, 30, 40))); + expect(buttonTheme.alignedDropdown, equals(true)); + expect(buttonTheme.layoutBehavior, equals(ButtonBarLayoutBehavior.constrained)); + }); - final Finder buttonBar = find.byType(ButtonBar); - expect(tester.getBottomRight(buttonBar).dy - tester.getTopRight(buttonBar).dy, 52.0); - }); - - testWidgets('ButtonBar has padding applied when using ButtonBarLayoutBehavior.padded', (WidgetTester tester) async { - await tester.pumpWidget( - SingleChildScrollView( - child: ListBody( - children: [ - ButtonTheme.bar( + testWidgets('ButtonBar button properties override ButtonBarTheme, defaults and ButtonTheme properties', (WidgetTester tester) async { + BuildContext capturedContext; + await tester.pumpWidget( + MaterialApp( + home: ButtonBarTheme( + data: const ButtonBarThemeData( + buttonTextTheme: ButtonTextTheme.accent, + buttonMinWidth: 4242.0, + buttonHeight: 8484.0, + buttonPadding: EdgeInsets.fromLTRB(50, 60, 70, 80), + buttonAlignedDropdown: false, layoutBehavior: ButtonBarLayoutBehavior.padded, - child: const Directionality( + ), + child: ButtonBar( + buttonTextTheme: ButtonTextTheme.primary, + buttonMinWidth: 42.0, + buttonHeight: 84.0, + buttonPadding: const EdgeInsets.fromLTRB(10, 20, 30, 40), + buttonAlignedDropdown: true, + layoutBehavior: ButtonBarLayoutBehavior.constrained, + children: [ + Builder(builder: (BuildContext context) { + capturedContext = context; + return Container(); + }) + ], + ), + ), + ) + ); + final ButtonThemeData buttonTheme = ButtonTheme.of(capturedContext); + expect(buttonTheme.textTheme, equals(ButtonTextTheme.primary)); + expect(buttonTheme.minWidth, equals(42.0)); + expect(buttonTheme.height, equals(84.0)); + expect(buttonTheme.padding, equals(const EdgeInsets.fromLTRB(10, 20, 30, 40))); + expect(buttonTheme.alignedDropdown, equals(true)); + expect(buttonTheme.layoutBehavior, equals(ButtonBarLayoutBehavior.constrained)); + }); + + }); + + group('layoutBehavior', () { + + testWidgets('ButtonBar has a min height of 52 when using ButtonBarLayoutBehavior.constrained', (WidgetTester tester) async { + await tester.pumpWidget( + SingleChildScrollView( + child: ListBody( + children: const [ + Directionality( textDirection: TextDirection.ltr, child: ButtonBar( + layoutBehavior: ButtonBarLayoutBehavior.constrained, children: [ SizedBox(width: 10.0, height: 10.0), ], ), ), - ), - ], + ], + ), ), - ), - ); + ); - final Finder buttonBar = find.byType(ButtonBar); - expect(tester.getBottomRight(buttonBar).dy - tester.getTopRight(buttonBar).dy, 26.0); - }); + final Finder buttonBar = find.byType(ButtonBar); + expect(tester.getBottomRight(buttonBar).dy - tester.getTopRight(buttonBar).dy, 52.0); + }); - testWidgets('ButtonBar FlatButton inherits Theme accentColor', (WidgetTester tester) async { - // Regression test for https://github.com/flutter/flutter/issues/22789 - - await tester.pumpWidget( - MaterialApp( - theme: ThemeData(accentColor: const Color(0x00000001)), - home: Builder( - builder: (BuildContext context) { - return Center( - child: ButtonTheme.bar( + testWidgets('ButtonBar has padding applied when using ButtonBarLayoutBehavior.padded', (WidgetTester tester) async { + await tester.pumpWidget( + SingleChildScrollView( + child: ListBody( + children: const [ + Directionality( + textDirection: TextDirection.ltr, child: ButtonBar( + layoutBehavior: ButtonBarLayoutBehavior.padded, children: [ - FlatButton( - child: const Text('button'), - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( // puts its actions in a ButtonBar - actions: [ - FlatButton( - onPressed: () { }, - child: const Text('enabled'), - ), - ], - ); - }, - ); - }, - ), + SizedBox(width: 10.0, height: 10.0), ], ), ), - ); - }, + ], + ), ), - ), - ); + ); - expect(tester.widget(find.byType(RawMaterialButton)).textStyle.color, const Color(0x00000001)); + final Finder buttonBar = find.byType(ButtonBar); + expect(tester.getBottomRight(buttonBar).dy - tester.getTopRight(buttonBar).dy, 26.0); + }); - // Show the dialog - await tester.tap(find.text('button')); - await tester.pumpAndSettle(); - - final Finder dialogButton = find.ancestor( - of: find.text('enabled'), - matching: find.byType(RawMaterialButton), - ); - expect(tester.widget(dialogButton).textStyle.color, const Color(0x00000001)); }); + } diff --git a/packages/flutter/test/material/button_bar_theme_test.dart b/packages/flutter/test/material/button_bar_theme_test.dart new file mode 100644 index 00000000000..d14da3a4c0d --- /dev/null +++ b/packages/flutter/test/material/button_bar_theme_test.dart @@ -0,0 +1,150 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + + test('ButtonBarThemeData null fields by default', () { + const ButtonBarThemeData buttonBarTheme = ButtonBarThemeData(); + expect(buttonBarTheme.alignment, null); + expect(buttonBarTheme.mainAxisSize, null); + expect(buttonBarTheme.buttonTextTheme, null); + expect(buttonBarTheme.buttonMinWidth, null); + expect(buttonBarTheme.buttonHeight, null); + expect(buttonBarTheme.buttonPadding, null); + expect(buttonBarTheme.buttonAlignedDropdown, null); + expect(buttonBarTheme.layoutBehavior, null); + }); + + test('ThemeData uses default ButtonBarThemeData', () { + expect(ThemeData().buttonBarTheme, equals(const ButtonBarThemeData())); + }); + + test('ButtonBarThemeData copyWith, ==, hashCode basics', () { + expect(const ButtonBarThemeData(), const ButtonBarThemeData().copyWith()); + expect(const ButtonBarThemeData().hashCode, const ButtonBarThemeData().copyWith().hashCode); + }); + + testWidgets('ButtonBarThemeData lerps correctly', (WidgetTester tester) async { + const ButtonBarThemeData barThemePrimary = ButtonBarThemeData( + alignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + buttonTextTheme: ButtonTextTheme.primary, + buttonMinWidth: 20.0, + buttonHeight: 20.0, + buttonPadding: EdgeInsets.symmetric(vertical: 5.0), + buttonAlignedDropdown: false, + layoutBehavior: ButtonBarLayoutBehavior.padded, + ); + const ButtonBarThemeData barThemeAccent = ButtonBarThemeData( + alignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + buttonTextTheme: ButtonTextTheme.accent, + buttonMinWidth: 10.0, + buttonHeight: 40.0, + buttonPadding: EdgeInsets.symmetric(horizontal: 10.0), + buttonAlignedDropdown: true, + layoutBehavior: ButtonBarLayoutBehavior.constrained, + ); + + final ButtonBarThemeData lerp = ButtonBarThemeData.lerp(barThemePrimary, barThemeAccent, 0.5); + expect(lerp.alignment, equals(MainAxisAlignment.center)); + expect(lerp.mainAxisSize, equals(MainAxisSize.max)); + expect(lerp.buttonTextTheme, equals(ButtonTextTheme.accent)); + expect(lerp.buttonMinWidth, equals(15.0)); + expect(lerp.buttonHeight, equals(30.0)); + expect(lerp.buttonPadding, equals(const EdgeInsets.fromLTRB(5.0, 2.5, 5.0, 2.5))); + expect(lerp.buttonAlignedDropdown, isTrue); + expect(lerp.layoutBehavior, equals(ButtonBarLayoutBehavior.constrained)); + }); + + testWidgets('Default ButtonBarThemeData debugFillProperties', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + const ButtonBarThemeData().debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, []); + }); + + testWidgets('ButtonBarThemeData implements debugFillProperties', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + const ButtonBarThemeData( + alignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + buttonTextTheme: ButtonTextTheme.accent, + buttonMinWidth: 10.0, + buttonHeight: 42.0, + buttonPadding: EdgeInsets.symmetric(horizontal: 7.3), + buttonAlignedDropdown: true, + layoutBehavior: ButtonBarLayoutBehavior.constrained, + ).debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, [ + 'alignment: MainAxisAlignment.center', + 'mainAxisSize: MainAxisSize.max', + 'textTheme: ButtonTextTheme.accent', + 'minWidth: 10.0', + 'height: 42.0', + 'padding: EdgeInsets(7.3, 0.0, 7.3, 0.0)', + 'dropdown width matches button', + 'layoutBehavior: ButtonBarLayoutBehavior.constrained', + ]); + }); + + testWidgets('ButtonBarTheme.of falls back to ThemeData.buttonBarTheme', (WidgetTester tester) async { + const ButtonBarThemeData buttonBarTheme = ButtonBarThemeData(buttonMinWidth: 42.0); + BuildContext capturedContext; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(buttonBarTheme: buttonBarTheme), + home: Builder( + builder: (BuildContext context) { + capturedContext = context; + return Container(); + } + ) + ) + ); + expect(ButtonBarTheme.of(capturedContext), equals(buttonBarTheme)); + expect(ButtonBarTheme.of(capturedContext).buttonMinWidth, equals(42.0)); + }); + + testWidgets('ButtonBarTheme overrides ThemeData.buttonBarTheme', (WidgetTester tester) async { + const ButtonBarThemeData defaultBarTheme = ButtonBarThemeData(buttonMinWidth: 42.0); + const ButtonBarThemeData buttonBarTheme = ButtonBarThemeData(buttonMinWidth: 84.0); + BuildContext capturedContext; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(buttonBarTheme: defaultBarTheme), + home: Builder( + builder: (BuildContext context) { + return ButtonBarTheme( + data: buttonBarTheme, + child: Builder( + builder: (BuildContext context) { + capturedContext = context; + return Container(); + }, + ), + ); + } + ) + ) + ); + expect(ButtonBarTheme.of(capturedContext), equals(buttonBarTheme)); + expect(ButtonBarTheme.of(capturedContext).buttonMinWidth, equals(84.0)); + }); +}