diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index 38bee62a28..7df8daf393 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -45,6 +45,10 @@ export 'src/material/checkbox.dart'; export 'src/material/checkbox_list_tile.dart'; export 'src/material/checkbox_theme.dart'; export 'src/material/chip.dart'; +export 'src/material/chip_action.dart'; +export 'src/material/chip_choice.dart'; +export 'src/material/chip_filter.dart'; +export 'src/material/chip_input.dart'; export 'src/material/chip_theme.dart'; export 'src/material/circle_avatar.dart'; export 'src/material/color_scheme.dart'; diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart index e4ae1d56c0..75b5ddf730 100644 --- a/packages/flutter/lib/src/material/chip.dart +++ b/packages/flutter/lib/src/material/chip.dart @@ -659,777 +659,6 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri } } -/// A material design input chip. -/// -/// Input chips represent a complex piece of information, such as an entity -/// (person, place, or thing) or conversational text, in a compact form. -/// -/// Input chips can be made selectable by setting [onSelected], deletable by -/// setting [onDeleted], and pressable like a button with [onPressed]. They have -/// a [label], and they can have a leading icon (see [avatar]) and a trailing -/// icon ([deleteIcon]). Colors and padding can be customized. -/// -/// Requires one of its ancestors to be a [Material] widget. -/// -/// Input chips work together with other UI elements. They can appear: -/// -/// * In a [Wrap] widget. -/// * In a horizontally scrollable list, like a [ListView] whose -/// scrollDirection is [Axis.horizontal]. -/// -/// {@tool snippet} -/// -/// ```dart -/// InputChip( -/// avatar: CircleAvatar( -/// backgroundColor: Colors.grey.shade800, -/// child: const Text('AB'), -/// ), -/// label: const Text('Aaron Burr'), -/// onPressed: () { -/// print('I am the one thing in life.'); -/// } -/// ) -/// ``` -/// {@end-tool} -/// -/// See also: -/// -/// * [Chip], a chip that displays information and can be deleted. -/// * [ChoiceChip], allows a single selection from a set of options. Choice -/// chips contain related descriptive text or categories. -/// * [FilterChip], uses tags or descriptive words as a way to filter content. -/// * [ActionChip], represents an action related to primary content. -/// * [CircleAvatar], which shows images or initials of people. -/// * [Wrap], A widget that displays its children in multiple horizontal or -/// vertical runs. -/// * -class InputChip extends StatelessWidget - implements - ChipAttributes, - DeletableChipAttributes, - SelectableChipAttributes, - CheckmarkableChipAttributes, - DisabledChipAttributes, - TappableChipAttributes { - /// Creates an [InputChip]. - /// - /// The [onPressed] and [onSelected] callbacks must not both be specified at - /// the same time. - /// - /// The [label], [isEnabled], [selected], [autofocus], and [clipBehavior] - /// arguments must not be null. The [pressElevation] and [elevation] must be - /// null or non-negative. Typically, [pressElevation] is greater than - /// [elevation]. - const InputChip({ - Key? key, - this.avatar, - required this.label, - this.labelStyle, - this.labelPadding, - this.selected = false, - this.isEnabled = true, - this.onSelected, - this.deleteIcon, - this.onDeleted, - this.deleteIconColor, - this.deleteButtonTooltipMessage, - this.onPressed, - this.pressElevation, - this.disabledColor, - this.selectedColor, - this.tooltip, - this.side, - this.shape, - this.clipBehavior = Clip.none, - this.focusNode, - this.autofocus = false, - this.backgroundColor, - this.padding, - this.visualDensity, - this.materialTapTargetSize, - this.elevation, - this.shadowColor, - this.selectedShadowColor, - this.showCheckmark, - this.checkmarkColor, - this.avatarBorder = const CircleBorder(), - @Deprecated( - 'Migrate to deleteButtonTooltipMessage. ' - 'This feature was deprecated after v2.10.0-0.3.pre.' - ) - this.useDeleteButtonTooltip = true, - }) : assert(selected != null), - assert(isEnabled != null), - assert(label != null), - assert(clipBehavior != null), - assert(autofocus != null), - assert(pressElevation == null || pressElevation >= 0.0), - assert(elevation == null || elevation >= 0.0), - super(key: key); - - @override - final Widget? avatar; - @override - final Widget label; - @override - final TextStyle? labelStyle; - @override - final EdgeInsetsGeometry? labelPadding; - @override - final bool selected; - @override - final bool isEnabled; - @override - final ValueChanged? onSelected; - @override - final Widget? deleteIcon; - @override - final VoidCallback? onDeleted; - @override - final Color? deleteIconColor; - @override - final String? deleteButtonTooltipMessage; - @override - final VoidCallback? onPressed; - @override - final double? pressElevation; - @override - final Color? disabledColor; - @override - final Color? selectedColor; - @override - final String? tooltip; - @override - final BorderSide? side; - @override - final OutlinedBorder? shape; - @override - final Clip clipBehavior; - @override - final FocusNode? focusNode; - @override - final bool autofocus; - @override - final Color? backgroundColor; - @override - final EdgeInsetsGeometry? padding; - @override - final VisualDensity? visualDensity; - @override - final MaterialTapTargetSize? materialTapTargetSize; - @override - final double? elevation; - @override - final Color? shadowColor; - @override - final Color? selectedShadowColor; - @override - final bool? showCheckmark; - @override - final Color? checkmarkColor; - @override - final ShapeBorder avatarBorder; - @override - @Deprecated( - 'Migrate to deleteButtonTooltipMessage. ' - 'This feature was deprecated after v2.10.0-0.3.pre.' - ) - final bool useDeleteButtonTooltip; - - @override - Widget build(BuildContext context) { - assert(debugCheckHasMaterial(context)); - return RawChip( - avatar: avatar, - label: label, - labelStyle: labelStyle, - labelPadding: labelPadding, - deleteIcon: deleteIcon, - onDeleted: onDeleted, - deleteIconColor: deleteIconColor, - useDeleteButtonTooltip: useDeleteButtonTooltip, - deleteButtonTooltipMessage: deleteButtonTooltipMessage, - onSelected: onSelected, - onPressed: onPressed, - pressElevation: pressElevation, - selected: selected, - disabledColor: disabledColor, - selectedColor: selectedColor, - tooltip: tooltip, - side: side, - shape: shape, - clipBehavior: clipBehavior, - focusNode: focusNode, - autofocus: autofocus, - backgroundColor: backgroundColor, - padding: padding, - visualDensity: visualDensity, - materialTapTargetSize: materialTapTargetSize, - elevation: elevation, - shadowColor: shadowColor, - selectedShadowColor: selectedShadowColor, - showCheckmark: showCheckmark, - checkmarkColor: checkmarkColor, - isEnabled: isEnabled && (onSelected != null || onDeleted != null || onPressed != null), - avatarBorder: avatarBorder, - ); - } -} - -/// A material design choice chip. -/// -/// [ChoiceChip]s represent a single choice from a set. Choice chips contain -/// related descriptive text or categories. -/// -/// Requires one of its ancestors to be a [Material] widget. The [selected] and -/// [label] arguments must not be null. -/// -/// {@tool snippet} -/// -/// ```dart -/// class MyThreeOptions extends StatefulWidget { -/// const MyThreeOptions({Key? key}) : super(key: key); -/// -/// @override -/// State createState() => _MyThreeOptionsState(); -/// } -/// -/// class _MyThreeOptionsState extends State { -/// int? _value = 1; -/// -/// @override -/// Widget build(BuildContext context) { -/// return Wrap( -/// children: List.generate( -/// 3, -/// (int index) { -/// return ChoiceChip( -/// label: Text('Item $index'), -/// selected: _value == index, -/// onSelected: (bool selected) { -/// setState(() { -/// _value = selected ? index : null; -/// }); -/// }, -/// ); -/// }, -/// ).toList(), -/// ); -/// } -/// } -/// ``` -/// {@end-tool} -/// -/// See also: -/// -/// * [Chip], a chip that displays information and can be deleted. -/// * [InputChip], a chip that represents a complex piece of information, such -/// as an entity (person, place, or thing) or conversational text, in a -/// compact form. -/// * [FilterChip], uses tags or descriptive words as a way to filter content. -/// * [ActionChip], represents an action related to primary content. -/// * [CircleAvatar], which shows images or initials of people. -/// * [Wrap], A widget that displays its children in multiple horizontal or -/// vertical runs. -/// * -class ChoiceChip extends StatelessWidget - implements - ChipAttributes, - SelectableChipAttributes, - DisabledChipAttributes { - /// Create a chip that acts like a radio button. - /// - /// The [label], [selected], [autofocus], and [clipBehavior] arguments must - /// not be null. The [pressElevation] and [elevation] must be null or - /// non-negative. Typically, [pressElevation] is greater than [elevation]. - const ChoiceChip({ - Key? key, - this.avatar, - required this.label, - this.labelStyle, - this.labelPadding, - this.onSelected, - this.pressElevation, - required this.selected, - this.selectedColor, - this.disabledColor, - this.tooltip, - this.side, - this.shape, - this.clipBehavior = Clip.none, - this.focusNode, - this.autofocus = false, - this.backgroundColor, - this.padding, - this.visualDensity, - this.materialTapTargetSize, - this.elevation, - this.shadowColor, - this.selectedShadowColor, - this.avatarBorder = const CircleBorder(), - }) : assert(selected != null), - assert(label != null), - assert(clipBehavior != null), - assert(autofocus != null), - assert(pressElevation == null || pressElevation >= 0.0), - assert(elevation == null || elevation >= 0.0), - super(key: key); - - @override - final Widget? avatar; - @override - final Widget label; - @override - final TextStyle? labelStyle; - @override - final EdgeInsetsGeometry? labelPadding; - @override - final ValueChanged? onSelected; - @override - final double? pressElevation; - @override - final bool selected; - @override - final Color? disabledColor; - @override - final Color? selectedColor; - @override - final String? tooltip; - @override - final BorderSide? side; - @override - final OutlinedBorder? shape; - @override - final Clip clipBehavior; - @override - final FocusNode? focusNode; - @override - final bool autofocus; - @override - final Color? backgroundColor; - @override - final EdgeInsetsGeometry? padding; - @override - final VisualDensity? visualDensity; - @override - final MaterialTapTargetSize? materialTapTargetSize; - @override - final double? elevation; - @override - final Color? shadowColor; - @override - final Color? selectedShadowColor; - @override - final ShapeBorder avatarBorder; - - @override - bool get isEnabled => onSelected != null; - - @override - Widget build(BuildContext context) { - assert(debugCheckHasMaterial(context)); - final ChipThemeData chipTheme = ChipTheme.of(context); - return RawChip( - avatar: avatar, - label: label, - labelStyle: labelStyle ?? (selected ? chipTheme.secondaryLabelStyle : null), - labelPadding: labelPadding, - onSelected: onSelected, - pressElevation: pressElevation, - selected: selected, - showCheckmark: false, - tooltip: tooltip, - side: side, - shape: shape, - clipBehavior: clipBehavior, - focusNode: focusNode, - autofocus: autofocus, - disabledColor: disabledColor, - selectedColor: selectedColor ?? chipTheme.secondarySelectedColor, - backgroundColor: backgroundColor, - padding: padding, - visualDensity: visualDensity, - isEnabled: isEnabled, - materialTapTargetSize: materialTapTargetSize, - elevation: elevation, - shadowColor: shadowColor, - selectedShadowColor: selectedShadowColor, - avatarBorder: avatarBorder, - ); - } -} - -/// A material design filter chip. -/// -/// Filter chips use tags or descriptive words as a way to filter content. -/// -/// Filter chips are a good alternative to [Checkbox] or [Switch] widgets. -/// Unlike these alternatives, filter chips allow for clearly delineated and -/// exposed options in a compact area. -/// -/// Requires one of its ancestors to be a [Material] widget. -/// -/// {@tool snippet} -/// -/// ```dart -/// class ActorFilterEntry { -/// const ActorFilterEntry(this.name, this.initials); -/// final String name; -/// final String initials; -/// } -/// -/// class CastFilter extends StatefulWidget { -/// const CastFilter({Key? key}) : super(key: key); -/// -/// @override -/// State createState() => CastFilterState(); -/// } -/// -/// class CastFilterState extends State { -/// final List _cast = [ -/// const ActorFilterEntry('Aaron Burr', 'AB'), -/// const ActorFilterEntry('Alexander Hamilton', 'AH'), -/// const ActorFilterEntry('Eliza Hamilton', 'EH'), -/// const ActorFilterEntry('James Madison', 'JM'), -/// ]; -/// final List _filters = []; -/// -/// Iterable get actorWidgets { -/// return _cast.map((ActorFilterEntry actor) { -/// return Padding( -/// padding: const EdgeInsets.all(4.0), -/// child: FilterChip( -/// avatar: CircleAvatar(child: Text(actor.initials)), -/// label: Text(actor.name), -/// selected: _filters.contains(actor.name), -/// onSelected: (bool value) { -/// setState(() { -/// if (value) { -/// _filters.add(actor.name); -/// } else { -/// _filters.removeWhere((String name) { -/// return name == actor.name; -/// }); -/// } -/// }); -/// }, -/// ), -/// ); -/// }); -/// } -/// -/// @override -/// Widget build(BuildContext context) { -/// return Column( -/// mainAxisAlignment: MainAxisAlignment.center, -/// children: [ -/// Wrap( -/// children: actorWidgets.toList(), -/// ), -/// Text('Look for: ${_filters.join(', ')}'), -/// ], -/// ); -/// } -/// } -/// ``` -/// {@end-tool} -/// -/// See also: -/// -/// * [Chip], a chip that displays information and can be deleted. -/// * [InputChip], a chip that represents a complex piece of information, such -/// as an entity (person, place, or thing) or conversational text, in a -/// compact form. -/// * [ChoiceChip], allows a single selection from a set of options. Choice -/// chips contain related descriptive text or categories. -/// * [ActionChip], represents an action related to primary content. -/// * [CircleAvatar], which shows images or initials of people. -/// * [Wrap], A widget that displays its children in multiple horizontal or -/// vertical runs. -/// * -class FilterChip extends StatelessWidget - implements - ChipAttributes, - SelectableChipAttributes, - CheckmarkableChipAttributes, - DisabledChipAttributes { - /// Create a chip that acts like a checkbox. - /// - /// The [selected], [label], [autofocus], and [clipBehavior] arguments must - /// not be null. The [pressElevation] and [elevation] must be null or - /// non-negative. Typically, [pressElevation] is greater than [elevation]. - const FilterChip({ - Key? key, - this.avatar, - required this.label, - this.labelStyle, - this.labelPadding, - this.selected = false, - required this.onSelected, - this.pressElevation, - this.disabledColor, - this.selectedColor, - this.tooltip, - this.side, - this.shape, - this.clipBehavior = Clip.none, - this.focusNode, - this.autofocus = false, - this.backgroundColor, - this.padding, - this.visualDensity, - this.materialTapTargetSize, - this.elevation, - this.shadowColor, - this.selectedShadowColor, - this.showCheckmark, - this.checkmarkColor, - this.avatarBorder = const CircleBorder(), - }) : assert(selected != null), - assert(label != null), - assert(clipBehavior != null), - assert(autofocus != null), - assert(pressElevation == null || pressElevation >= 0.0), - assert(elevation == null || elevation >= 0.0), - super(key: key); - - @override - final Widget? avatar; - @override - final Widget label; - @override - final TextStyle? labelStyle; - @override - final EdgeInsetsGeometry? labelPadding; - @override - final bool selected; - @override - final ValueChanged? onSelected; - @override - final double? pressElevation; - @override - final Color? disabledColor; - @override - final Color? selectedColor; - @override - final String? tooltip; - @override - final BorderSide? side; - @override - final OutlinedBorder? shape; - @override - final Clip clipBehavior; - @override - final FocusNode? focusNode; - @override - final bool autofocus; - @override - final Color? backgroundColor; - @override - final EdgeInsetsGeometry? padding; - @override - final VisualDensity? visualDensity; - @override - final MaterialTapTargetSize? materialTapTargetSize; - @override - final double? elevation; - @override - final Color? shadowColor; - @override - final Color? selectedShadowColor; - @override - final bool? showCheckmark; - @override - final Color? checkmarkColor; - @override - final ShapeBorder avatarBorder; - - @override - bool get isEnabled => onSelected != null; - - @override - Widget build(BuildContext context) { - assert(debugCheckHasMaterial(context)); - return RawChip( - avatar: avatar, - label: label, - labelStyle: labelStyle, - labelPadding: labelPadding, - onSelected: onSelected, - pressElevation: pressElevation, - selected: selected, - tooltip: tooltip, - side: side, - shape: shape, - clipBehavior: clipBehavior, - focusNode: focusNode, - autofocus: autofocus, - backgroundColor: backgroundColor, - disabledColor: disabledColor, - selectedColor: selectedColor, - padding: padding, - visualDensity: visualDensity, - isEnabled: isEnabled, - materialTapTargetSize: materialTapTargetSize, - elevation: elevation, - shadowColor: shadowColor, - selectedShadowColor: selectedShadowColor, - showCheckmark: showCheckmark, - checkmarkColor: checkmarkColor, - avatarBorder: avatarBorder, - ); - } -} - -/// A material design action chip. -/// -/// Action chips are a set of options which trigger an action related to primary -/// content. Action chips should appear dynamically and contextually in a UI. -/// -/// Action chips can be tapped to trigger an action or show progress and -/// confirmation. They cannot be disabled; if the action is not applicable, the -/// chip should not be included in the interface. (This contrasts with buttons, -/// where unavailable choices are usually represented as disabled controls.) -/// -/// Action chips are displayed after primary content, such as below a card or -/// persistently at the bottom of a screen. -/// -/// The material button widgets, [ElevatedButton], [TextButton], and -/// [OutlinedButton], are an alternative to action chips, which should appear -/// statically and consistently in a UI. -/// -/// Requires one of its ancestors to be a [Material] widget. -/// -/// {@tool snippet} -/// -/// ```dart -/// ActionChip( -/// avatar: CircleAvatar( -/// backgroundColor: Colors.grey.shade800, -/// child: const Text('AB'), -/// ), -/// label: const Text('Aaron Burr'), -/// onPressed: () { -/// print('If you stand for nothing, Burr, what’ll you fall for?'); -/// } -/// ) -/// ``` -/// {@end-tool} -/// -/// See also: -/// -/// * [Chip], a chip that displays information and can be deleted. -/// * [InputChip], a chip that represents a complex piece of information, such -/// as an entity (person, place, or thing) or conversational text, in a -/// compact form. -/// * [ChoiceChip], allows a single selection from a set of options. Choice -/// chips contain related descriptive text or categories. -/// * [CircleAvatar], which shows images or initials of people. -/// * [Wrap], A widget that displays its children in multiple horizontal or -/// vertical runs. -/// * -class ActionChip extends StatelessWidget implements ChipAttributes, TappableChipAttributes { - /// Create a chip that acts like a button. - /// - /// The [label], [onPressed], [autofocus], and [clipBehavior] arguments must - /// not be null. The [pressElevation] and [elevation] must be null or - /// non-negative. Typically, [pressElevation] is greater than [elevation]. - const ActionChip({ - Key? key, - this.avatar, - required this.label, - this.labelStyle, - this.labelPadding, - required this.onPressed, - this.pressElevation, - this.tooltip, - this.side, - this.shape, - this.clipBehavior = Clip.none, - this.focusNode, - this.autofocus = false, - this.backgroundColor, - this.padding, - this.visualDensity, - this.materialTapTargetSize, - this.elevation, - this.shadowColor, - }) : assert(label != null), - assert(clipBehavior != null), - assert(autofocus != null), - assert( - onPressed != null, - 'Rather than disabling an ActionChip by setting onPressed to null, ' - 'remove it from the interface entirely.', - ), - assert(pressElevation == null || pressElevation >= 0.0), - assert(elevation == null || elevation >= 0.0), - super(key: key); - - @override - final Widget? avatar; - @override - final Widget label; - @override - final TextStyle? labelStyle; - @override - final EdgeInsetsGeometry? labelPadding; - @override - final VoidCallback onPressed; - @override - final double? pressElevation; - @override - final String? tooltip; - @override - final BorderSide? side; - @override - final OutlinedBorder? shape; - @override - final Clip clipBehavior; - @override - final FocusNode? focusNode; - @override - final bool autofocus; - @override - final Color? backgroundColor; - @override - final EdgeInsetsGeometry? padding; - @override - final VisualDensity? visualDensity; - @override - final MaterialTapTargetSize? materialTapTargetSize; - @override - final double? elevation; - @override - final Color? shadowColor; - - @override - Widget build(BuildContext context) { - assert(debugCheckHasMaterial(context)); - return RawChip( - avatar: avatar, - label: label, - onPressed: onPressed, - pressElevation: pressElevation, - tooltip: tooltip, - labelStyle: labelStyle, - backgroundColor: backgroundColor, - side: side, - shape: shape, - clipBehavior: clipBehavior, - focusNode: focusNode, - autofocus: autofocus, - padding: padding, - visualDensity: visualDensity, - labelPadding: labelPadding, - materialTapTargetSize: materialTapTargetSize, - elevation: elevation, - shadowColor: shadowColor, - ); - } -} - /// A raw material design chip. /// /// This serves as the basis for all of the chip widget types to aggregate. diff --git a/packages/flutter/lib/src/material/chip_action.dart b/packages/flutter/lib/src/material/chip_action.dart new file mode 100644 index 0000000000..533928e223 --- /dev/null +++ b/packages/flutter/lib/src/material/chip_action.dart @@ -0,0 +1,157 @@ +// 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/widgets.dart'; + +import 'chip.dart'; +import 'debug.dart'; +import 'theme_data.dart'; + +/// A material design action chip. +/// +/// Action chips are a set of options which trigger an action related to primary +/// content. Action chips should appear dynamically and contextually in a UI. +/// +/// Action chips can be tapped to trigger an action or show progress and +/// confirmation. They cannot be disabled; if the action is not applicable, the +/// chip should not be included in the interface. (This contrasts with buttons, +/// where unavailable choices are usually represented as disabled controls.) +/// +/// Action chips are displayed after primary content, such as below a card or +/// persistently at the bottom of a screen. +/// +/// The material button widgets, [ElevatedButton], [TextButton], and +/// [OutlinedButton], are an alternative to action chips, which should appear +/// statically and consistently in a UI. +/// +/// Requires one of its ancestors to be a [Material] widget. +/// +/// {@tool snippet} +/// +/// ```dart +/// ActionChip( +/// avatar: CircleAvatar( +/// backgroundColor: Colors.grey.shade800, +/// child: const Text('AB'), +/// ), +/// label: const Text('Aaron Burr'), +/// onPressed: () { +/// print('If you stand for nothing, Burr, what’ll you fall for?'); +/// } +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [Chip], a chip that displays information and can be deleted. +/// * [InputChip], a chip that represents a complex piece of information, such +/// as an entity (person, place, or thing) or conversational text, in a +/// compact form. +/// * [ChoiceChip], allows a single selection from a set of options. Choice +/// chips contain related descriptive text or categories. +/// * [CircleAvatar], which shows images or initials of people. +/// * [Wrap], A widget that displays its children in multiple horizontal or +/// vertical runs. +/// * +class ActionChip extends StatelessWidget implements ChipAttributes, TappableChipAttributes { + /// Create a chip that acts like a button. + /// + /// The [label], [onPressed], [autofocus], and [clipBehavior] arguments must + /// not be null. The [pressElevation] and [elevation] must be null or + /// non-negative. Typically, [pressElevation] is greater than [elevation]. + const ActionChip({ + Key? key, + this.avatar, + required this.label, + this.labelStyle, + this.labelPadding, + required this.onPressed, + this.pressElevation, + this.tooltip, + this.side, + this.shape, + this.clipBehavior = Clip.none, + this.focusNode, + this.autofocus = false, + this.backgroundColor, + this.padding, + this.visualDensity, + this.materialTapTargetSize, + this.elevation, + this.shadowColor, + }) : assert(label != null), + assert(clipBehavior != null), + assert(autofocus != null), + assert( + onPressed != null, + 'Rather than disabling an ActionChip by setting onPressed to null, ' + 'remove it from the interface entirely.', + ), + assert(pressElevation == null || pressElevation >= 0.0), + assert(elevation == null || elevation >= 0.0), + super(key: key); + + @override + final Widget? avatar; + @override + final Widget label; + @override + final TextStyle? labelStyle; + @override + final EdgeInsetsGeometry? labelPadding; + @override + final VoidCallback onPressed; + @override + final double? pressElevation; + @override + final String? tooltip; + @override + final BorderSide? side; + @override + final OutlinedBorder? shape; + @override + final Clip clipBehavior; + @override + final FocusNode? focusNode; + @override + final bool autofocus; + @override + final Color? backgroundColor; + @override + final EdgeInsetsGeometry? padding; + @override + final VisualDensity? visualDensity; + @override + final MaterialTapTargetSize? materialTapTargetSize; + @override + final double? elevation; + @override + final Color? shadowColor; + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterial(context)); + return RawChip( + avatar: avatar, + label: label, + onPressed: onPressed, + pressElevation: pressElevation, + tooltip: tooltip, + labelStyle: labelStyle, + backgroundColor: backgroundColor, + side: side, + shape: shape, + clipBehavior: clipBehavior, + focusNode: focusNode, + autofocus: autofocus, + padding: padding, + visualDensity: visualDensity, + labelPadding: labelPadding, + materialTapTargetSize: materialTapTargetSize, + elevation: elevation, + shadowColor: shadowColor, + ); + } +} diff --git a/packages/flutter/lib/src/material/chip_choice.dart b/packages/flutter/lib/src/material/chip_choice.dart new file mode 100644 index 0000000000..29bf293177 --- /dev/null +++ b/packages/flutter/lib/src/material/chip_choice.dart @@ -0,0 +1,193 @@ +// 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/widgets.dart'; + +import 'chip.dart'; +import 'chip_theme.dart'; +import 'debug.dart'; +import 'theme_data.dart'; + +/// A material design choice chip. +/// +/// [ChoiceChip]s represent a single choice from a set. Choice chips contain +/// related descriptive text or categories. +/// +/// Requires one of its ancestors to be a [Material] widget. The [selected] and +/// [label] arguments must not be null. +/// +/// {@tool snippet} +/// +/// ```dart +/// class MyThreeOptions extends StatefulWidget { +/// const MyThreeOptions({Key? key}) : super(key: key); +/// +/// @override +/// State createState() => _MyThreeOptionsState(); +/// } +/// +/// class _MyThreeOptionsState extends State { +/// int? _value = 1; +/// +/// @override +/// Widget build(BuildContext context) { +/// return Wrap( +/// children: List.generate( +/// 3, +/// (int index) { +/// return ChoiceChip( +/// label: Text('Item $index'), +/// selected: _value == index, +/// onSelected: (bool selected) { +/// setState(() { +/// _value = selected ? index : null; +/// }); +/// }, +/// ); +/// }, +/// ).toList(), +/// ); +/// } +/// } +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [Chip], a chip that displays information and can be deleted. +/// * [InputChip], a chip that represents a complex piece of information, such +/// as an entity (person, place, or thing) or conversational text, in a +/// compact form. +/// * [FilterChip], uses tags or descriptive words as a way to filter content. +/// * [ActionChip], represents an action related to primary content. +/// * [CircleAvatar], which shows images or initials of people. +/// * [Wrap], A widget that displays its children in multiple horizontal or +/// vertical runs. +/// * +class ChoiceChip extends StatelessWidget + implements + ChipAttributes, + SelectableChipAttributes, + DisabledChipAttributes { + /// Create a chip that acts like a radio button. + /// + /// The [label], [selected], [autofocus], and [clipBehavior] arguments must + /// not be null. The [pressElevation] and [elevation] must be null or + /// non-negative. Typically, [pressElevation] is greater than [elevation]. + const ChoiceChip({ + Key? key, + this.avatar, + required this.label, + this.labelStyle, + this.labelPadding, + this.onSelected, + this.pressElevation, + required this.selected, + this.selectedColor, + this.disabledColor, + this.tooltip, + this.side, + this.shape, + this.clipBehavior = Clip.none, + this.focusNode, + this.autofocus = false, + this.backgroundColor, + this.padding, + this.visualDensity, + this.materialTapTargetSize, + this.elevation, + this.shadowColor, + this.selectedShadowColor, + this.avatarBorder = const CircleBorder(), + }) : assert(selected != null), + assert(label != null), + assert(clipBehavior != null), + assert(autofocus != null), + assert(pressElevation == null || pressElevation >= 0.0), + assert(elevation == null || elevation >= 0.0), + super(key: key); + + @override + final Widget? avatar; + @override + final Widget label; + @override + final TextStyle? labelStyle; + @override + final EdgeInsetsGeometry? labelPadding; + @override + final ValueChanged? onSelected; + @override + final double? pressElevation; + @override + final bool selected; + @override + final Color? disabledColor; + @override + final Color? selectedColor; + @override + final String? tooltip; + @override + final BorderSide? side; + @override + final OutlinedBorder? shape; + @override + final Clip clipBehavior; + @override + final FocusNode? focusNode; + @override + final bool autofocus; + @override + final Color? backgroundColor; + @override + final EdgeInsetsGeometry? padding; + @override + final VisualDensity? visualDensity; + @override + final MaterialTapTargetSize? materialTapTargetSize; + @override + final double? elevation; + @override + final Color? shadowColor; + @override + final Color? selectedShadowColor; + @override + final ShapeBorder avatarBorder; + + @override + bool get isEnabled => onSelected != null; + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterial(context)); + final ChipThemeData chipTheme = ChipTheme.of(context); + return RawChip( + avatar: avatar, + label: label, + labelStyle: labelStyle ?? (selected ? chipTheme.secondaryLabelStyle : null), + labelPadding: labelPadding, + onSelected: onSelected, + pressElevation: pressElevation, + selected: selected, + showCheckmark: false, + tooltip: tooltip, + side: side, + shape: shape, + clipBehavior: clipBehavior, + focusNode: focusNode, + autofocus: autofocus, + disabledColor: disabledColor, + selectedColor: selectedColor ?? chipTheme.secondarySelectedColor, + backgroundColor: backgroundColor, + padding: padding, + visualDensity: visualDensity, + isEnabled: isEnabled, + materialTapTargetSize: materialTapTargetSize, + elevation: elevation, + shadowColor: shadowColor, + selectedShadowColor: selectedShadowColor, + avatarBorder: avatarBorder, + ); + } +} diff --git a/packages/flutter/lib/src/material/chip_filter.dart b/packages/flutter/lib/src/material/chip_filter.dart new file mode 100644 index 0000000000..15b7a3b08e --- /dev/null +++ b/packages/flutter/lib/src/material/chip_filter.dart @@ -0,0 +1,231 @@ +// 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/widgets.dart'; + +import 'chip.dart'; +import 'debug.dart'; +import 'theme_data.dart'; + +/// A material design filter chip. +/// +/// Filter chips use tags or descriptive words as a way to filter content. +/// +/// Filter chips are a good alternative to [Checkbox] or [Switch] widgets. +/// Unlike these alternatives, filter chips allow for clearly delineated and +/// exposed options in a compact area. +/// +/// Requires one of its ancestors to be a [Material] widget. +/// +/// {@tool snippet} +/// +/// ```dart +/// class ActorFilterEntry { +/// const ActorFilterEntry(this.name, this.initials); +/// final String name; +/// final String initials; +/// } +/// +/// class CastFilter extends StatefulWidget { +/// const CastFilter({Key? key}) : super(key: key); +/// +/// @override +/// State createState() => CastFilterState(); +/// } +/// +/// class CastFilterState extends State { +/// final List _cast = [ +/// const ActorFilterEntry('Aaron Burr', 'AB'), +/// const ActorFilterEntry('Alexander Hamilton', 'AH'), +/// const ActorFilterEntry('Eliza Hamilton', 'EH'), +/// const ActorFilterEntry('James Madison', 'JM'), +/// ]; +/// final List _filters = []; +/// +/// Iterable get actorWidgets { +/// return _cast.map((ActorFilterEntry actor) { +/// return Padding( +/// padding: const EdgeInsets.all(4.0), +/// child: FilterChip( +/// avatar: CircleAvatar(child: Text(actor.initials)), +/// label: Text(actor.name), +/// selected: _filters.contains(actor.name), +/// onSelected: (bool value) { +/// setState(() { +/// if (value) { +/// _filters.add(actor.name); +/// } else { +/// _filters.removeWhere((String name) { +/// return name == actor.name; +/// }); +/// } +/// }); +/// }, +/// ), +/// ); +/// }); +/// } +/// +/// @override +/// Widget build(BuildContext context) { +/// return Column( +/// mainAxisAlignment: MainAxisAlignment.center, +/// children: [ +/// Wrap( +/// children: actorWidgets.toList(), +/// ), +/// Text('Look for: ${_filters.join(', ')}'), +/// ], +/// ); +/// } +/// } +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [Chip], a chip that displays information and can be deleted. +/// * [InputChip], a chip that represents a complex piece of information, such +/// as an entity (person, place, or thing) or conversational text, in a +/// compact form. +/// * [ChoiceChip], allows a single selection from a set of options. Choice +/// chips contain related descriptive text or categories. +/// * [ActionChip], represents an action related to primary content. +/// * [CircleAvatar], which shows images or initials of people. +/// * [Wrap], A widget that displays its children in multiple horizontal or +/// vertical runs. +/// * +class FilterChip extends StatelessWidget + implements + ChipAttributes, + SelectableChipAttributes, + CheckmarkableChipAttributes, + DisabledChipAttributes { + /// Create a chip that acts like a checkbox. + /// + /// The [selected], [label], [autofocus], and [clipBehavior] arguments must + /// not be null. The [pressElevation] and [elevation] must be null or + /// non-negative. Typically, [pressElevation] is greater than [elevation]. + const FilterChip({ + Key? key, + this.avatar, + required this.label, + this.labelStyle, + this.labelPadding, + this.selected = false, + required this.onSelected, + this.pressElevation, + this.disabledColor, + this.selectedColor, + this.tooltip, + this.side, + this.shape, + this.clipBehavior = Clip.none, + this.focusNode, + this.autofocus = false, + this.backgroundColor, + this.padding, + this.visualDensity, + this.materialTapTargetSize, + this.elevation, + this.shadowColor, + this.selectedShadowColor, + this.showCheckmark, + this.checkmarkColor, + this.avatarBorder = const CircleBorder(), + }) : assert(selected != null), + assert(label != null), + assert(clipBehavior != null), + assert(autofocus != null), + assert(pressElevation == null || pressElevation >= 0.0), + assert(elevation == null || elevation >= 0.0), + super(key: key); + + @override + final Widget? avatar; + @override + final Widget label; + @override + final TextStyle? labelStyle; + @override + final EdgeInsetsGeometry? labelPadding; + @override + final bool selected; + @override + final ValueChanged? onSelected; + @override + final double? pressElevation; + @override + final Color? disabledColor; + @override + final Color? selectedColor; + @override + final String? tooltip; + @override + final BorderSide? side; + @override + final OutlinedBorder? shape; + @override + final Clip clipBehavior; + @override + final FocusNode? focusNode; + @override + final bool autofocus; + @override + final Color? backgroundColor; + @override + final EdgeInsetsGeometry? padding; + @override + final VisualDensity? visualDensity; + @override + final MaterialTapTargetSize? materialTapTargetSize; + @override + final double? elevation; + @override + final Color? shadowColor; + @override + final Color? selectedShadowColor; + @override + final bool? showCheckmark; + @override + final Color? checkmarkColor; + @override + final ShapeBorder avatarBorder; + + @override + bool get isEnabled => onSelected != null; + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterial(context)); + return RawChip( + avatar: avatar, + label: label, + labelStyle: labelStyle, + labelPadding: labelPadding, + onSelected: onSelected, + pressElevation: pressElevation, + selected: selected, + tooltip: tooltip, + side: side, + shape: shape, + clipBehavior: clipBehavior, + focusNode: focusNode, + autofocus: autofocus, + backgroundColor: backgroundColor, + disabledColor: disabledColor, + selectedColor: selectedColor, + padding: padding, + visualDensity: visualDensity, + isEnabled: isEnabled, + materialTapTargetSize: materialTapTargetSize, + elevation: elevation, + shadowColor: shadowColor, + selectedShadowColor: selectedShadowColor, + showCheckmark: showCheckmark, + checkmarkColor: checkmarkColor, + avatarBorder: avatarBorder, + ); + } +} diff --git a/packages/flutter/lib/src/material/chip_input.dart b/packages/flutter/lib/src/material/chip_input.dart new file mode 100644 index 0000000000..7c7b19f055 --- /dev/null +++ b/packages/flutter/lib/src/material/chip_input.dart @@ -0,0 +1,227 @@ +// 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/widgets.dart'; + +import 'chip.dart'; +import 'debug.dart'; +import 'theme_data.dart'; + +/// A material design input chip. +/// +/// Input chips represent a complex piece of information, such as an entity +/// (person, place, or thing) or conversational text, in a compact form. +/// +/// Input chips can be made selectable by setting [onSelected], deletable by +/// setting [onDeleted], and pressable like a button with [onPressed]. They have +/// a [label], and they can have a leading icon (see [avatar]) and a trailing +/// icon ([deleteIcon]). Colors and padding can be customized. +/// +/// Requires one of its ancestors to be a [Material] widget. +/// +/// Input chips work together with other UI elements. They can appear: +/// +/// * In a [Wrap] widget. +/// * In a horizontally scrollable list, like a [ListView] whose +/// scrollDirection is [Axis.horizontal]. +/// +/// {@tool snippet} +/// +/// ```dart +/// InputChip( +/// avatar: CircleAvatar( +/// backgroundColor: Colors.grey.shade800, +/// child: const Text('AB'), +/// ), +/// label: const Text('Aaron Burr'), +/// onPressed: () { +/// print('I am the one thing in life.'); +/// } +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [Chip], a chip that displays information and can be deleted. +/// * [ChoiceChip], allows a single selection from a set of options. Choice +/// chips contain related descriptive text or categories. +/// * [FilterChip], uses tags or descriptive words as a way to filter content. +/// * [ActionChip], represents an action related to primary content. +/// * [CircleAvatar], which shows images or initials of people. +/// * [Wrap], A widget that displays its children in multiple horizontal or +/// vertical runs. +/// * +class InputChip extends StatelessWidget + implements + ChipAttributes, + DeletableChipAttributes, + SelectableChipAttributes, + CheckmarkableChipAttributes, + DisabledChipAttributes, + TappableChipAttributes { + /// Creates an [InputChip]. + /// + /// The [onPressed] and [onSelected] callbacks must not both be specified at + /// the same time. + /// + /// The [label], [isEnabled], [selected], [autofocus], and [clipBehavior] + /// arguments must not be null. The [pressElevation] and [elevation] must be + /// null or non-negative. Typically, [pressElevation] is greater than + /// [elevation]. + const InputChip({ + Key? key, + this.avatar, + required this.label, + this.labelStyle, + this.labelPadding, + this.selected = false, + this.isEnabled = true, + this.onSelected, + this.deleteIcon, + this.onDeleted, + this.deleteIconColor, + this.deleteButtonTooltipMessage, + this.onPressed, + this.pressElevation, + this.disabledColor, + this.selectedColor, + this.tooltip, + this.side, + this.shape, + this.clipBehavior = Clip.none, + this.focusNode, + this.autofocus = false, + this.backgroundColor, + this.padding, + this.visualDensity, + this.materialTapTargetSize, + this.elevation, + this.shadowColor, + this.selectedShadowColor, + this.showCheckmark, + this.checkmarkColor, + this.avatarBorder = const CircleBorder(), + @Deprecated( + 'Migrate to deleteButtonTooltipMessage. ' + 'This feature was deprecated after v2.10.0-0.3.pre.' + ) + this.useDeleteButtonTooltip = true, + }) : assert(selected != null), + assert(isEnabled != null), + assert(label != null), + assert(clipBehavior != null), + assert(autofocus != null), + assert(pressElevation == null || pressElevation >= 0.0), + assert(elevation == null || elevation >= 0.0), + super(key: key); + + @override + final Widget? avatar; + @override + final Widget label; + @override + final TextStyle? labelStyle; + @override + final EdgeInsetsGeometry? labelPadding; + @override + final bool selected; + @override + final bool isEnabled; + @override + final ValueChanged? onSelected; + @override + final Widget? deleteIcon; + @override + final VoidCallback? onDeleted; + @override + final Color? deleteIconColor; + @override + final String? deleteButtonTooltipMessage; + @override + final VoidCallback? onPressed; + @override + final double? pressElevation; + @override + final Color? disabledColor; + @override + final Color? selectedColor; + @override + final String? tooltip; + @override + final BorderSide? side; + @override + final OutlinedBorder? shape; + @override + final Clip clipBehavior; + @override + final FocusNode? focusNode; + @override + final bool autofocus; + @override + final Color? backgroundColor; + @override + final EdgeInsetsGeometry? padding; + @override + final VisualDensity? visualDensity; + @override + final MaterialTapTargetSize? materialTapTargetSize; + @override + final double? elevation; + @override + final Color? shadowColor; + @override + final Color? selectedShadowColor; + @override + final bool? showCheckmark; + @override + final Color? checkmarkColor; + @override + final ShapeBorder avatarBorder; + @override + @Deprecated( + 'Migrate to deleteButtonTooltipMessage. ' + 'This feature was deprecated after v2.10.0-0.3.pre.' + ) + final bool useDeleteButtonTooltip; + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterial(context)); + return RawChip( + avatar: avatar, + label: label, + labelStyle: labelStyle, + labelPadding: labelPadding, + deleteIcon: deleteIcon, + onDeleted: onDeleted, + deleteIconColor: deleteIconColor, + useDeleteButtonTooltip: useDeleteButtonTooltip, + deleteButtonTooltipMessage: deleteButtonTooltipMessage, + onSelected: onSelected, + onPressed: onPressed, + pressElevation: pressElevation, + selected: selected, + disabledColor: disabledColor, + selectedColor: selectedColor, + tooltip: tooltip, + side: side, + shape: shape, + clipBehavior: clipBehavior, + focusNode: focusNode, + autofocus: autofocus, + backgroundColor: backgroundColor, + padding: padding, + visualDensity: visualDensity, + materialTapTargetSize: materialTapTargetSize, + elevation: elevation, + shadowColor: shadowColor, + selectedShadowColor: selectedShadowColor, + showCheckmark: showCheckmark, + checkmarkColor: checkmarkColor, + isEnabled: isEnabled && (onSelected != null || onDeleted != null || onPressed != null), + avatarBorder: avatarBorder, + ); + } +} diff --git a/packages/flutter/test/material/chip_action_test.dart b/packages/flutter/test/material/chip_action_test.dart new file mode 100644 index 0000000000..c2e71e5362 --- /dev/null +++ b/packages/flutter/test/material/chip_action_test.dart @@ -0,0 +1,61 @@ +// 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_test/flutter_test.dart'; + +/// Adds the basic requirements for a Chip. +Widget wrapForChip({ + required Widget child, + TextDirection textDirection = TextDirection.ltr, + double textScaleFactor = 1.0, + Brightness brightness = Brightness.light, +}) { + return MaterialApp( + theme: ThemeData(brightness: brightness), + home: Directionality( + textDirection: textDirection, + child: MediaQuery( + data: MediaQueryData.fromWindow(WidgetsBinding.instance.window).copyWith(textScaleFactor: textScaleFactor), + child: Material(child: child), + ), + ), + ); +} + +void checkChipMaterialClipBehavior(WidgetTester tester, Clip clipBehavior) { + final Iterable materials = tester.widgetList(find.byType(Material)); + // There should be two Material widgets, first Material is from the "_wrapForChip" and + // last Material is from the "RawChip". + expect(materials.length, 2); + // The last Material from `RawChip` should have the clip behavior. + expect(materials.last.clipBehavior, clipBehavior); +} + +void main() { + testWidgets('ActionChip can be tapped', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: ActionChip( + onPressed: () { }, + label: const Text('action chip'), + ), + ), + ), + ); + + await tester.tap(find.byType(ActionChip)); + expect(tester.takeException(), null); + }); + + testWidgets('ActionChip clipBehavior properly passes through to the Material', (WidgetTester tester) async { + const Text label = Text('label'); + await tester.pumpWidget(wrapForChip(child: ActionChip(label: label, onPressed: () { }))); + checkChipMaterialClipBehavior(tester, Clip.none); + + await tester.pumpWidget(wrapForChip(child: ActionChip(label: label, clipBehavior: Clip.antiAlias, onPressed: () { }))); + checkChipMaterialClipBehavior(tester, Clip.antiAlias); + }); +} diff --git a/packages/flutter/test/material/chip_choice_test.dart b/packages/flutter/test/material/chip_choice_test.dart new file mode 100644 index 0000000000..fe2af679ab --- /dev/null +++ b/packages/flutter/test/material/chip_choice_test.dart @@ -0,0 +1,123 @@ +// 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_test/flutter_test.dart'; + +import '../rendering/mock_canvas.dart'; + +RenderBox getMaterialBox(WidgetTester tester) { + return tester.firstRenderObject( + find.descendant( + of: find.byType(RawChip), + matching: find.byType(CustomPaint), + ), + ); +} + +Material getMaterial(WidgetTester tester) { + return tester.widget( + find.descendant( + of: find.byType(RawChip), + matching: find.byType(Material), + ), + ); +} + +DefaultTextStyle getLabelStyle(WidgetTester tester, String labelText) { + return tester.widget( + find.ancestor( + of: find.text(labelText), + matching: find.byType(DefaultTextStyle), + ).first, + ); +} + +/// Adds the basic requirements for a Chip. +Widget wrapForChip({ + required Widget child, + TextDirection textDirection = TextDirection.ltr, + double textScaleFactor = 1.0, + Brightness brightness = Brightness.light, +}) { + return MaterialApp( + theme: ThemeData(brightness: brightness), + home: Directionality( + textDirection: textDirection, + child: MediaQuery( + data: MediaQueryData.fromWindow(WidgetsBinding.instance.window).copyWith(textScaleFactor: textScaleFactor), + child: Material(child: child), + ), + ), + ); +} + +void checkChipMaterialClipBehavior(WidgetTester tester, Clip clipBehavior) { + final Iterable materials = tester.widgetList(find.byType(Material)); + // There should be two Material widgets, first Material is from the "_wrapForChip" and + // last Material is from the "RawChip". + expect(materials.length, 2); + // The last Material from `RawChip` should have the clip behavior. + expect(materials.last.clipBehavior, clipBehavior); +} + +void main() { + testWidgets('ChoiceChip defaults', (WidgetTester tester) async { + Widget buildFrame(Brightness brightness) { + return MaterialApp( + theme: ThemeData(brightness: brightness), + home: const Scaffold( + body: Center( + child: ChoiceChip( + label: Text('Chip A'), + selected: true, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildFrame(Brightness.light)); + expect(getMaterialBox(tester), paints..path(color: const Color(0x3d000000))); + expect(tester.getSize(find.byType(ChoiceChip)), const Size(108.0, 48.0)); + expect(getMaterial(tester).color, null); + expect(getMaterial(tester).elevation, 0); + expect(getMaterial(tester).shape, const StadiumBorder()); + expect(getLabelStyle(tester, 'Chip A').style.color?.value, 0xde000000); + + await tester.pumpWidget(buildFrame(Brightness.dark)); + await tester.pumpAndSettle(); // Theme transition animation + expect(getMaterialBox(tester), paints..path(color: const Color(0x3dffffff))); + expect(tester.getSize(find.byType(ChoiceChip)), const Size(108.0, 48.0)); + expect(getMaterial(tester).color, null); + expect(getMaterial(tester).elevation, 0); + expect(getMaterial(tester).shape, const StadiumBorder()); + expect(getLabelStyle(tester, 'Chip A').style.color?.value, 0xdeffffff); + }); + + testWidgets('ChoiceChip can be tapped', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: ChoiceChip( + selected: false, + label: Text('choice chip'), + ), + ), + ), + ); + + await tester.tap(find.byType(ChoiceChip)); + expect(tester.takeException(), null); + }); + + testWidgets('ChoiceChip clipBehavior properly passes through to the Material', (WidgetTester tester) async { + const Text label = Text('label'); + await tester.pumpWidget(wrapForChip(child: const ChoiceChip(label: label, selected: false))); + checkChipMaterialClipBehavior(tester, Clip.none); + + await tester.pumpWidget(wrapForChip(child: const ChoiceChip(label: label, selected: false, clipBehavior: Clip.antiAlias))); + checkChipMaterialClipBehavior(tester, Clip.antiAlias); + }); +} diff --git a/packages/flutter/test/material/chip_filter_test.dart b/packages/flutter/test/material/chip_filter_test.dart new file mode 100644 index 0000000000..3405f3b6fc --- /dev/null +++ b/packages/flutter/test/material/chip_filter_test.dart @@ -0,0 +1,173 @@ +// 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_test/flutter_test.dart'; + +import '../rendering/mock_canvas.dart'; + +/// Adds the basic requirements for a Chip. +Widget wrapForChip({ + required Widget child, + TextDirection textDirection = TextDirection.ltr, + double textScaleFactor = 1.0, + Brightness brightness = Brightness.light, +}) { + return MaterialApp( + theme: ThemeData(brightness: brightness), + home: Directionality( + textDirection: textDirection, + child: MediaQuery( + data: MediaQueryData.fromWindow(WidgetsBinding.instance.window).copyWith(textScaleFactor: textScaleFactor), + child: Material(child: child), + ), + ), + ); +} + +Future pumpCheckmarkChip( + WidgetTester tester, { + required Widget chip, + Color? themeColor, + Brightness brightness = Brightness.light, +}) async { + await tester.pumpWidget( + wrapForChip( + brightness: brightness, + child: Builder( + builder: (BuildContext context) { + final ChipThemeData chipTheme = ChipTheme.of(context); + return ChipTheme( + data: themeColor == null ? chipTheme : chipTheme.copyWith( + checkmarkColor: themeColor, + ), + child: chip, + ); + }, + ), + ), + ); +} + +Widget selectedFilterChip({ Color? checkmarkColor }) { + return FilterChip( + label: const Text('InputChip'), + selected: true, + showCheckmark: true, + checkmarkColor: checkmarkColor, + onSelected: (bool _) { }, + ); +} + +void expectCheckmarkColor(Finder finder, Color color) { + expect( + finder, + paints + // The first path that is painted is the selection overlay. We do not care + // how it is painted but it has to be added it to this pattern so that the + // check mark can be checked next. + ..path() + // The second path that is painted is the check mark. + ..path(color: color), + ); +} + +void checkChipMaterialClipBehavior(WidgetTester tester, Clip clipBehavior) { + final Iterable materials = tester.widgetList(find.byType(Material)); + // There should be two Material widgets, first Material is from the "_wrapForChip" and + // last Material is from the "RawChip". + expect(materials.length, 2); + // The last Material from `RawChip` should have the clip behavior. + expect(materials.last.clipBehavior, clipBehavior); +} + +void main() { + testWidgets('FilterChip can be tapped', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: FilterChip( + onSelected: (bool valueChanged) { }, + label: const Text('filter chip'), + ), + ), + ), + ); + + await tester.tap(find.byType(FilterChip)); + expect(tester.takeException(), null); + }); + + testWidgets('Filter chip check mark color is determined by platform brightness when light', (WidgetTester tester) async { + await pumpCheckmarkChip( + tester, + chip: selectedFilterChip(), + ); + + expectCheckmarkColor( + find.byType(FilterChip), + Colors.black.withAlpha(0xde), + ); + }); + + testWidgets('Filter chip check mark color is determined by platform brightness when dark', (WidgetTester tester) async { + await pumpCheckmarkChip( + tester, + chip: selectedFilterChip(), + brightness: Brightness.dark, + ); + + expectCheckmarkColor( + find.byType(FilterChip), + Colors.white.withAlpha(0xde), + ); + }); + + testWidgets('Filter chip check mark color can be set by the chip theme', (WidgetTester tester) async { + await pumpCheckmarkChip( + tester, + chip: selectedFilterChip(), + themeColor: const Color(0xff00ff00), + ); + + expectCheckmarkColor( + find.byType(FilterChip), + const Color(0xff00ff00), + ); + }); + + testWidgets('Filter chip check mark color can be set by the chip constructor', (WidgetTester tester) async { + await pumpCheckmarkChip( + tester, + chip: selectedFilterChip(checkmarkColor: const Color(0xff00ff00)), + ); + + expectCheckmarkColor( + find.byType(FilterChip), + const Color(0xff00ff00), + ); + }); + + testWidgets('Filter chip check mark color is set by chip constructor even when a theme color is specified', (WidgetTester tester) async { + await pumpCheckmarkChip( + tester, + chip: selectedFilterChip(checkmarkColor: const Color(0xffff0000)), + themeColor: const Color(0xff00ff00), + ); + + expectCheckmarkColor( + find.byType(FilterChip), + const Color(0xffff0000), + ); + }); + + testWidgets('FilterChip clipBehavior properly passes through to the Material', (WidgetTester tester) async { + const Text label = Text('label'); + await tester.pumpWidget(wrapForChip(child: FilterChip(label: label, onSelected: (bool b) { }))); + checkChipMaterialClipBehavior(tester, Clip.none); + + await tester.pumpWidget(wrapForChip(child: FilterChip(label: label, onSelected: (bool b) { }, clipBehavior: Clip.antiAlias))); + checkChipMaterialClipBehavior(tester, Clip.antiAlias); + }); +} diff --git a/packages/flutter/test/material/chip_input_test.dart b/packages/flutter/test/material/chip_input_test.dart new file mode 100644 index 0000000000..84929d3eae --- /dev/null +++ b/packages/flutter/test/material/chip_input_test.dart @@ -0,0 +1,237 @@ +// 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_test/flutter_test.dart'; + +import '../rendering/mock_canvas.dart'; + +/// Adds the basic requirements for a Chip. +Widget wrapForChip({ + required Widget child, + TextDirection textDirection = TextDirection.ltr, + double textScaleFactor = 1.0, + Brightness brightness = Brightness.light, +}) { + return MaterialApp( + theme: ThemeData(brightness: brightness), + home: Directionality( + textDirection: textDirection, + child: MediaQuery( + data: MediaQueryData.fromWindow(WidgetsBinding.instance.window).copyWith(textScaleFactor: textScaleFactor), + child: Material(child: child), + ), + ), + ); +} + +Widget selectedInputChip({ Color? checkmarkColor }) { + return InputChip( + label: const Text('InputChip'), + selected: true, + showCheckmark: true, + checkmarkColor: checkmarkColor, + ); +} + + +Future pumpCheckmarkChip( + WidgetTester tester, { + required Widget chip, + Color? themeColor, + Brightness brightness = Brightness.light, +}) async { + await tester.pumpWidget( + wrapForChip( + brightness: brightness, + child: Builder( + builder: (BuildContext context) { + final ChipThemeData chipTheme = ChipTheme.of(context); + return ChipTheme( + data: themeColor == null ? chipTheme : chipTheme.copyWith( + checkmarkColor: themeColor, + ), + child: chip, + ); + }, + ), + ), + ); +} + +void expectCheckmarkColor(Finder finder, Color color) { + expect( + finder, + paints + // The first path that is painted is the selection overlay. We do not care + // how it is painted but it has to be added it to this pattern so that the + // check mark can be checked next. + ..path() + // The second path that is painted is the check mark. + ..path(color: color), + ); +} + +void checkChipMaterialClipBehavior(WidgetTester tester, Clip clipBehavior) { + final Iterable materials = tester.widgetList(find.byType(Material)); + // There should be two Material widgets, first Material is from the "_wrapForChip" and + // last Material is from the "RawChip". + expect(materials.length, 2); + // The last Material from `RawChip` should have the clip behavior. + expect(materials.last.clipBehavior, clipBehavior); +} + +void main() { + testWidgets('InputChip can be tapped', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: InputChip( + label: Text('input chip'), + ), + ), + ), + ); + + await tester.tap(find.byType(InputChip)); + expect(tester.takeException(), null); + }); + + testWidgets('loses focus when disabled', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(debugLabel: 'InputChip'); + await tester.pumpWidget( + wrapForChip( + child: InputChip( + focusNode: focusNode, + autofocus: true, + shape: const RoundedRectangleBorder(), + avatar: const CircleAvatar(child: Text('A')), + label: const Text('Chip A'), + onPressed: () { }, + ), + ), + ); + await tester.pump(); + expect(focusNode.hasPrimaryFocus, isTrue); + + await tester.pumpWidget( + wrapForChip( + child: InputChip( + focusNode: focusNode, + autofocus: true, + shape: const RoundedRectangleBorder(), + avatar: const CircleAvatar(child: Text('A')), + label: const Text('Chip A'), + ), + ), + ); + await tester.pump(); + expect(focusNode.hasPrimaryFocus, isFalse); + }); + + testWidgets('cannot be traversed to when disabled', (WidgetTester tester) async { + final FocusNode focusNode1 = FocusNode(debugLabel: 'InputChip 1'); + final FocusNode focusNode2 = FocusNode(debugLabel: 'InputChip 2'); + await tester.pumpWidget( + wrapForChip( + child: Column( + children: [ + InputChip( + focusNode: focusNode1, + autofocus: true, + label: const Text('Chip A'), + onPressed: () { }, + ), + InputChip( + focusNode: focusNode2, + autofocus: true, + label: const Text('Chip B'), + ), + ], + ), + ), + ); + await tester.pump(); + expect(focusNode1.hasPrimaryFocus, isTrue); + expect(focusNode2.hasPrimaryFocus, isFalse); + + expect(focusNode1.nextFocus(), isTrue); + + await tester.pump(); + expect(focusNode1.hasPrimaryFocus, isTrue); + expect(focusNode2.hasPrimaryFocus, isFalse); + }); + + testWidgets('Input chip check mark color is determined by platform brightness when light', (WidgetTester tester) async { + await pumpCheckmarkChip( + tester, + chip: selectedInputChip(), + ); + + expectCheckmarkColor( + find.byType(InputChip), + Colors.black.withAlpha(0xde), + ); + }); + + testWidgets('Input chip check mark color is determined by platform brightness when dark', (WidgetTester tester) async { + await pumpCheckmarkChip( + tester, + chip: selectedInputChip(), + brightness: Brightness.dark, + ); + + expectCheckmarkColor( + find.byType(InputChip), + Colors.white.withAlpha(0xde), + ); + }); + + testWidgets('Input chip check mark color can be set by the chip theme', (WidgetTester tester) async { + await pumpCheckmarkChip( + tester, + chip: selectedInputChip(), + themeColor: const Color(0xff00ff00), + ); + + expectCheckmarkColor( + find.byType(InputChip), + const Color(0xff00ff00), + ); + }); + + testWidgets('Input chip check mark color can be set by the chip constructor', (WidgetTester tester) async { + await pumpCheckmarkChip( + tester, + chip: selectedInputChip(checkmarkColor: const Color(0xff00ff00)), + ); + + expectCheckmarkColor( + find.byType(InputChip), + const Color(0xff00ff00), + ); + }); + + testWidgets('Input chip check mark color is set by chip constructor even when a theme color is specified', (WidgetTester tester) async { + await pumpCheckmarkChip( + tester, + chip: selectedInputChip(checkmarkColor: const Color(0xffff0000)), + themeColor: const Color(0xff00ff00), + ); + + expectCheckmarkColor( + find.byType(InputChip), + const Color(0xffff0000), + ); + }); + + testWidgets('InputChip clipBehavior properly passes through to the Material', (WidgetTester tester) async { + const Text label = Text('label'); + await tester.pumpWidget(wrapForChip(child: const InputChip(label: label))); + checkChipMaterialClipBehavior(tester, Clip.none); + + await tester.pumpWidget(wrapForChip(child: const InputChip(label: label, clipBehavior: Clip.antiAlias))); + checkChipMaterialClipBehavior(tester, Clip.antiAlias); + }); +} diff --git a/packages/flutter/test/material/chip_test.dart b/packages/flutter/test/material/chip_test.dart index 781baeac90..80cb5ccec0 100644 --- a/packages/flutter/test/material/chip_test.dart +++ b/packages/flutter/test/material/chip_test.dart @@ -71,7 +71,7 @@ double getDeleteDrawerProgress(WidgetTester tester) => getRenderChip(tester)?.de double getEnableProgress(WidgetTester tester) => getRenderChip(tester)?.enableAnimation?.value as double; /// Adds the basic requirements for a Chip. -Widget _wrapForChip({ +Widget wrapForChip({ required Widget child, TextDirection textDirection = TextDirection.ltr, double textScaleFactor = 1.0, @@ -93,7 +93,7 @@ Widget _wrapForChip({ /// further constraining the size of its child, the label widget. /// Optionally, adding an avatar or delete icon to the chip should not /// cause the chip or label to exceed its constrained height. -Future _testConstrainedLabel( +Future testConstrainedLabel( WidgetTester tester, { CircleAvatar? avatar, VoidCallback? onDeleted, @@ -105,7 +105,7 @@ Future _testConstrainedLabel( final Key labelKey = UniqueKey(); await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Center( child: SizedBox( width: chipParentWidth, @@ -133,65 +133,9 @@ Future _testConstrainedLabel( expect(chipSize.height, chipParentHeight); } -Widget _selectedInputChip({ Color? checkmarkColor }) { - return InputChip( - label: const Text('InputChip'), - selected: true, - showCheckmark: true, - checkmarkColor: checkmarkColor, - ); -} +void doNothing() {} -Widget _selectedFilterChip({ Color? checkmarkColor }) { - return FilterChip( - label: const Text('InputChip'), - selected: true, - showCheckmark: true, - checkmarkColor: checkmarkColor, - onSelected: (bool _) { }, - ); -} - -Future _pumpCheckmarkChip( - WidgetTester tester, { - required Widget chip, - Color? themeColor, - Brightness brightness = Brightness.light, -}) async { - await tester.pumpWidget( - _wrapForChip( - brightness: brightness, - child: Builder( - builder: (BuildContext context) { - final ChipThemeData chipTheme = ChipTheme.of(context); - return ChipTheme( - data: themeColor == null ? chipTheme : chipTheme.copyWith( - checkmarkColor: themeColor, - ), - child: chip, - ); - }, - ), - ), - ); -} - -void _expectCheckmarkColor(Finder finder, Color color) { - expect( - finder, - paints - // The first path that is painted is the selection overlay. We do not care - // how it is painted but it has to be added it to this pattern so that the - // check mark can be checked next. - ..path() - // The second path that is painted is the check mark. - ..path(color: color), - ); -} - -void _doNothing() {} - -Widget _chipWithOptionalDeleteButton({ +Widget chipWithOptionalDeleteButton({ Key? deleteButtonKey, Key? labelKey, required bool deletable, @@ -199,16 +143,16 @@ Widget _chipWithOptionalDeleteButton({ bool useDeleteButtonTooltip = true, String? chipTooltip, String? deleteButtonTooltipMessage, - VoidCallback? onPressed = _doNothing, + VoidCallback? onPressed = doNothing, }) { - return _wrapForChip( + return wrapForChip( textDirection: textDirection, child: Wrap( children: [ RawChip( tooltip: chipTooltip, onPressed: onPressed, - onDeleted: deletable ? _doNothing : null, + onDeleted: deletable ? doNothing : null, deleteIcon: Icon(Icons.close, key: deleteButtonKey), useDeleteButtonTooltip: useDeleteButtonTooltip, deleteButtonTooltipMessage: deleteButtonTooltipMessage, @@ -347,44 +291,11 @@ void main() { expect(labelStyle.wordSpacing, textTheme.bodyText1?.wordSpacing); }); - testWidgets('ChoiceChip defaults', (WidgetTester tester) async { - Widget buildFrame(Brightness brightness) { - return MaterialApp( - theme: ThemeData(brightness: brightness), - home: const Scaffold( - body: Center( - child: ChoiceChip( - label: Text('Chip A'), - selected: true, - ), - ), - ), - ); - } - - await tester.pumpWidget(buildFrame(Brightness.light)); - expect(getMaterialBox(tester), paints..path(color: const Color(0x3d000000))); - expect(tester.getSize(find.byType(ChoiceChip)), const Size(108.0, 48.0)); - expect(getMaterial(tester).color, null); - expect(getMaterial(tester).elevation, 0); - expect(getMaterial(tester).shape, const StadiumBorder()); - expect(getLabelStyle(tester, 'Chip A').style.color?.value, 0xde000000); - - await tester.pumpWidget(buildFrame(Brightness.dark)); - await tester.pumpAndSettle(); // Theme transition animation - expect(getMaterialBox(tester), paints..path(color: const Color(0x3dffffff))); - expect(tester.getSize(find.byType(ChoiceChip)), const Size(108.0, 48.0)); - expect(getMaterial(tester).color, null); - expect(getMaterial(tester).elevation, 0); - expect(getMaterial(tester).shape, const StadiumBorder()); - expect(getLabelStyle(tester, 'Chip A').style.color?.value, 0xdeffffff); - }); - testWidgets('Chip control test', (WidgetTester tester) async { final FeedbackTester feedback = FeedbackTester(); final List deletedChipLabels = []; await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Column( children: [ Chip( @@ -438,7 +349,7 @@ void main() { final Key labelKey = UniqueKey(); await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Center( child: SizedBox( width: 500.0, @@ -469,7 +380,7 @@ void main() { 'Chip constrains the size of the label widget when it exceeds the ' 'available space', (WidgetTester tester) async { - await _testConstrainedLabel(tester); + await testConstrainedLabel(tester); }, ); @@ -477,7 +388,7 @@ void main() { 'Chip constrains the size of the label widget when it exceeds the ' 'available space and the avatar is present', (WidgetTester tester) async { - await _testConstrainedLabel( + await testConstrainedLabel( tester, avatar: const CircleAvatar(child: Text('A')), ); @@ -488,7 +399,7 @@ void main() { 'Chip constrains the size of the label widget when it exceeds the ' 'available space and the delete icon is present', (WidgetTester tester) async { - await _testConstrainedLabel( + await testConstrainedLabel( tester, onDeleted: () { }, ); @@ -499,7 +410,7 @@ void main() { 'Chip constrains the size of the label widget when it exceeds the ' 'available space and both avatar and delete icons are present', (WidgetTester tester) async { - await _testConstrainedLabel( + await testConstrainedLabel( tester, avatar: const CircleAvatar(child: Text('A')), onDeleted: () { }, @@ -585,7 +496,7 @@ void main() { testWidgets('Chip in row works ok', (WidgetTester tester) async { const TextStyle style = TextStyle(fontFamily: 'Ahem', fontSize: 10.0); await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Row( children: const [ Chip(label: Text('Test'), labelStyle: style), @@ -596,7 +507,7 @@ void main() { expect(tester.getSize(find.byType(Text)), const Size(40.0, 10.0)); expect(tester.getSize(find.byType(Chip)), const Size(64.0, 48.0)); await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Row( children: const [ Flexible(child: Chip(label: Text('Test'), labelStyle: style)), @@ -607,7 +518,7 @@ void main() { expect(tester.getSize(find.byType(Text)), const Size(40.0, 10.0)); expect(tester.getSize(find.byType(Chip)), const Size(64.0, 48.0)); await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Row( children: const [ Expanded(child: Chip(label: Text('Test'), labelStyle: style)), @@ -621,7 +532,7 @@ void main() { testWidgets('Chip responds to materialTapTargetSize', (WidgetTester tester) async { await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Column( children: const [ Chip( @@ -645,7 +556,7 @@ void main() { final UniqueKey deleteKey = UniqueKey(); bool calledDelete = false; await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Column( children: [ Chip( @@ -670,7 +581,7 @@ void main() { calledDelete = false; await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Column( children: [ Chip( @@ -719,7 +630,7 @@ void main() { ); await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: test, textDirection: TextDirection.rtl, ), @@ -727,7 +638,7 @@ void main() { await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(tester.getCenter(find.text('ABC')).dx, greaterThan(tester.getCenter(find.byKey(iconKey)).dx)); await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: test, ), ); @@ -737,7 +648,7 @@ void main() { testWidgets('Chip responds to textScaleFactor', (WidgetTester tester) async { await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Column( children: const [ Chip( @@ -767,7 +678,7 @@ void main() { expect(tester.getSize(find.byType(Chip).last), anyOf(const Size(132.0, 48.0), const Size(131.0, 48.0))); await tester.pumpWidget( - _wrapForChip( + wrapForChip( textScaleFactor: 3.0, child: Column( children: const [ @@ -795,7 +706,7 @@ void main() { // Check that individual text scales are taken into account. await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Column( children: const [ Chip( @@ -824,7 +735,7 @@ void main() { final Key keyA = GlobalKey(); final Key keyB = GlobalKey(); await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Column( children: [ Chip( @@ -857,7 +768,7 @@ void main() { testWidgets('Avatars can be non-circle avatar widgets', (WidgetTester tester) async { final Key keyA = GlobalKey(); await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Column( children: [ Chip( @@ -875,7 +786,7 @@ void main() { testWidgets('Delete icons can be non-icon widgets', (WidgetTester tester) async { final Key keyA = GlobalKey(); await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Column( children: [ Chip( @@ -895,7 +806,7 @@ void main() { final GlobalKey keyA = GlobalKey(); final GlobalKey keyB = GlobalKey(); await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Overlay( initialEntries: [ OverlayEntry( @@ -931,7 +842,7 @@ void main() { final GlobalKey keyA = GlobalKey(); final GlobalKey keyB = GlobalKey(); await tester.pumpWidget( - _wrapForChip( + wrapForChip( textDirection: TextDirection.rtl, child: Overlay( initialEntries: [ @@ -969,7 +880,7 @@ void main() { final GlobalKey labelKey = GlobalKey(); Future pushChip({ Widget? avatar }) async { return tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Wrap( children: [ RawChip( @@ -1083,7 +994,7 @@ void main() { bool wasDeleted = false; Future pushChip({ bool deletable = false }) async { return tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Wrap( children: [ StatefulBuilder(builder: (BuildContext context, StateSetter setState) { @@ -1199,7 +1110,7 @@ void main() { bool deletePressed = false; await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Wrap( children: [ RawChip( @@ -1234,7 +1145,7 @@ void main() { final UniqueKey deleteButtonKey = UniqueKey(); await tester.pumpWidget( - _chipWithOptionalDeleteButton( + chipWithOptionalDeleteButton( labelKey: labelKey, deleteButtonKey: deleteButtonKey, deletable: true, @@ -1283,7 +1194,7 @@ void main() { final GlobalKey deleteButtonKey = GlobalKey(); await tester.pumpWidget( - _chipWithOptionalDeleteButton( + chipWithOptionalDeleteButton( labelKey: labelKey, deleteButtonKey: deleteButtonKey, deletable: true, @@ -1316,7 +1227,7 @@ void main() { final UniqueKey deleteButtonKey = UniqueKey(); await tester.pumpWidget( - _chipWithOptionalDeleteButton( + chipWithOptionalDeleteButton( labelKey: labelKey, deleteButtonKey: deleteButtonKey, deletable: true, @@ -1369,7 +1280,7 @@ void main() { final UniqueKey deleteButtonKey = UniqueKey(); await tester.pumpWidget( - _chipWithOptionalDeleteButton( + chipWithOptionalDeleteButton( labelKey: labelKey, onPressed: null, deleteButtonKey: deleteButtonKey, @@ -1424,7 +1335,7 @@ void main() { final UniqueKey deleteButtonKey = UniqueKey(); await tester.pumpWidget( - _chipWithOptionalDeleteButton( + chipWithOptionalDeleteButton( labelKey: labelKey, deleteButtonKey: deleteButtonKey, deletable: true, @@ -1453,7 +1364,7 @@ void main() { final UniqueKey labelKey = UniqueKey(); await tester.pumpWidget( - _chipWithOptionalDeleteButton( + chipWithOptionalDeleteButton( labelKey: labelKey, deletable: false, ), @@ -1507,7 +1418,7 @@ void main() { final UniqueKey labelKey = UniqueKey(); Future pushChip({ Widget? avatar, bool selectable = false }) async { return tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Wrap( children: [ StatefulBuilder(builder: (BuildContext context, StateSetter setState) { @@ -1587,7 +1498,7 @@ void main() { final UniqueKey labelKey = UniqueKey(); Future pushChip({ bool selectable = false }) async { return tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Wrap( children: [ StatefulBuilder(builder: (BuildContext context, StateSetter setState) { @@ -1660,7 +1571,7 @@ void main() { final UniqueKey labelKey = UniqueKey(); Future pushChip({ Widget? avatar, bool selectable = false }) async { return tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Wrap( children: [ StatefulBuilder(builder: (BuildContext context, StateSetter setState) { @@ -1719,7 +1630,7 @@ void main() { final ChipThemeData chipTheme = theme.chipTheme; Widget buildChip(ChipThemeData data) { - return _wrapForChip( + return wrapForChip( child: Theme( data: theme, child: const InputChip( @@ -1748,7 +1659,7 @@ void main() { ); Widget buildChip() { - return _wrapForChip( + return wrapForChip( child: Theme( data: theme, child: const Chip( @@ -1769,7 +1680,7 @@ void main() { testWidgets('ChipTheme labelStyle with inherit:true', (WidgetTester tester) async { Widget buildChip() { - return _wrapForChip( + return wrapForChip( child: Theme( data: ThemeData.light().copyWith( chipTheme: const ChipThemeData( @@ -1789,7 +1700,7 @@ void main() { testWidgets('Chip does not merge inherit:false label style with the theme label style', (WidgetTester tester) async { Widget buildChip() { - return _wrapForChip( + return wrapForChip( child: Theme( data: ThemeData(fontFamily: 'MyFont'), child: const DefaultTextStyle( @@ -1814,7 +1725,7 @@ void main() { testWidgets('Chip size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { final Key key1 = UniqueKey(); await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Theme( data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded), child: Center( @@ -1831,7 +1742,7 @@ void main() { final Key key2 = UniqueKey(); await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Theme( data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), child: Center( @@ -1868,7 +1779,7 @@ void main() { bool showCheckmark = true, }) { chipTheme ??= defaultChipTheme; - return _wrapForChip( + return wrapForChip( child: Theme( data: themeData, child: ChipTheme( @@ -2450,7 +2361,7 @@ void main() { testWidgets('can be tapped outside of chip delete icon', (WidgetTester tester) async { bool deleted = false; await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Row( children: [ Chip( @@ -2474,20 +2385,6 @@ void main() { }); testWidgets('Chips can be tapped', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Material( - child: ChoiceChip( - selected: false, - label: Text('choice chip'), - ), - ), - ), - ); - - await tester.tap(find.byType(ChoiceChip)); - expect(tester.takeException(), null); - await tester.pumpWidget( const MaterialApp( home: Material( @@ -2500,47 +2397,6 @@ void main() { await tester.tap(find.byType(RawChip)); expect(tester.takeException(), null); - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: ActionChip( - onPressed: () { }, - label: const Text('action chip'), - ), - ), - ), - ); - - await tester.tap(find.byType(ActionChip)); - expect(tester.takeException(), null); - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: FilterChip( - onSelected: (bool valueChanged) { }, - label: const Text('filter chip'), - ), - ), - ), - ); - - await tester.tap(find.byType(FilterChip)); - expect(tester.takeException(), null); - - await tester.pumpWidget( - const MaterialApp( - home: Material( - child: InputChip( - label: Text('input chip'), - ), - ), - ), - ); - - await tester.tap(find.byType(InputChip)); - expect(tester.takeException(), null); }); testWidgets('Chip elevation and shadow color work correctly', (WidgetTester tester) async { @@ -2554,7 +2410,7 @@ void main() { InputChip inputChip = const InputChip(label: Text('Label')); Widget buildChip(ChipThemeData data) { - return _wrapForChip( + return wrapForChip( child: Theme( data: theme, child: inputChip, @@ -2596,7 +2452,7 @@ void main() { testWidgets('can be tapped outside of chip body', (WidgetTester tester) async { bool pressed = false; await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: Row( children: [ InputChip( @@ -2620,7 +2476,7 @@ void main() { testWidgets('is hitTestable', (WidgetTester tester) async { await tester.pumpWidget( - _wrapForChip( + wrapForChip( child: InputChip( shape: const RoundedRectangleBorder(), avatar: const CircleAvatar(child: Text('A')), @@ -2641,51 +2497,15 @@ void main() { testWidgets('Chip clipBehavior properly passes through to the Material', (WidgetTester tester) async { const Text label = Text('label'); - await tester.pumpWidget(_wrapForChip(child: const Chip(label: label))); + await tester.pumpWidget(wrapForChip(child: const Chip(label: label))); checkChipMaterialClipBehavior(tester, Clip.none); - await tester.pumpWidget(_wrapForChip(child: const Chip(label: label, clipBehavior: Clip.antiAlias))); - checkChipMaterialClipBehavior(tester, Clip.antiAlias); - }); - - testWidgets('ChoiceChip clipBehavior properly passes through to the Material', (WidgetTester tester) async { - const Text label = Text('label'); - await tester.pumpWidget(_wrapForChip(child: const ChoiceChip(label: label, selected: false))); - checkChipMaterialClipBehavior(tester, Clip.none); - - await tester.pumpWidget(_wrapForChip(child: const ChoiceChip(label: label, selected: false, clipBehavior: Clip.antiAlias))); - checkChipMaterialClipBehavior(tester, Clip.antiAlias); - }); - - testWidgets('FilterChip clipBehavior properly passes through to the Material', (WidgetTester tester) async { - const Text label = Text('label'); - await tester.pumpWidget(_wrapForChip(child: FilterChip(label: label, onSelected: (bool b) { }))); - checkChipMaterialClipBehavior(tester, Clip.none); - - await tester.pumpWidget(_wrapForChip(child: FilterChip(label: label, onSelected: (bool b) { }, clipBehavior: Clip.antiAlias))); - checkChipMaterialClipBehavior(tester, Clip.antiAlias); - }); - - testWidgets('ActionChip clipBehavior properly passes through to the Material', (WidgetTester tester) async { - const Text label = Text('label'); - await tester.pumpWidget(_wrapForChip(child: ActionChip(label: label, onPressed: () { }))); - checkChipMaterialClipBehavior(tester, Clip.none); - - await tester.pumpWidget(_wrapForChip(child: ActionChip(label: label, clipBehavior: Clip.antiAlias, onPressed: () { }))); - checkChipMaterialClipBehavior(tester, Clip.antiAlias); - }); - - testWidgets('InputChip clipBehavior properly passes through to the Material', (WidgetTester tester) async { - const Text label = Text('label'); - await tester.pumpWidget(_wrapForChip(child: const InputChip(label: label))); - checkChipMaterialClipBehavior(tester, Clip.none); - - await tester.pumpWidget(_wrapForChip(child: const InputChip(label: label, clipBehavior: Clip.antiAlias))); + await tester.pumpWidget(wrapForChip(child: const Chip(label: label, clipBehavior: Clip.antiAlias))); checkChipMaterialClipBehavior(tester, Clip.antiAlias); }); testWidgets('selected chip and avatar draw darkened layer within avatar circle', (WidgetTester tester) async { - await tester.pumpWidget(_wrapForChip(child: const FilterChip( + await tester.pumpWidget(wrapForChip(child: const FilterChip( avatar: CircleAvatar(child: Text('t')), label: Text('test'), selected: true, @@ -3198,71 +3018,6 @@ void main() { expect(find.byType(RawChip), paints..drrect(color: selectedBorderSide.color)); }); - testWidgets('loses focus when disabled', (WidgetTester tester) async { - final FocusNode focusNode = FocusNode(debugLabel: 'InputChip'); - await tester.pumpWidget( - _wrapForChip( - child: InputChip( - focusNode: focusNode, - autofocus: true, - shape: const RoundedRectangleBorder(), - avatar: const CircleAvatar(child: Text('A')), - label: const Text('Chip A'), - onPressed: () { }, - ), - ), - ); - await tester.pump(); - expect(focusNode.hasPrimaryFocus, isTrue); - - await tester.pumpWidget( - _wrapForChip( - child: InputChip( - focusNode: focusNode, - autofocus: true, - shape: const RoundedRectangleBorder(), - avatar: const CircleAvatar(child: Text('A')), - label: const Text('Chip A'), - ), - ), - ); - await tester.pump(); - expect(focusNode.hasPrimaryFocus, isFalse); - }); - - testWidgets('cannot be traversed to when disabled', (WidgetTester tester) async { - final FocusNode focusNode1 = FocusNode(debugLabel: 'InputChip 1'); - final FocusNode focusNode2 = FocusNode(debugLabel: 'InputChip 2'); - await tester.pumpWidget( - _wrapForChip( - child: Column( - children: [ - InputChip( - focusNode: focusNode1, - autofocus: true, - label: const Text('Chip A'), - onPressed: () { }, - ), - InputChip( - focusNode: focusNode2, - autofocus: true, - label: const Text('Chip B'), - ), - ], - ), - ), - ); - await tester.pump(); - expect(focusNode1.hasPrimaryFocus, isTrue); - expect(focusNode2.hasPrimaryFocus, isFalse); - - expect(focusNode1.nextFocus(), isTrue); - - await tester.pump(); - expect(focusNode1.hasPrimaryFocus, isTrue); - expect(focusNode2.hasPrimaryFocus, isFalse); - }); - testWidgets('Chip responds to density changes.', (WidgetTester tester) async { const Key key = Key('test'); const Key textKey = Key('test text'); @@ -3368,135 +3123,9 @@ void main() { expect(box.size, equals(const Size(128, 24.0 + 16.0))); }); - testWidgets('Input chip check mark color is determined by platform brightness when light', (WidgetTester tester) async { - await _pumpCheckmarkChip( - tester, - chip: _selectedInputChip(), - ); - - _expectCheckmarkColor( - find.byType(InputChip), - Colors.black.withAlpha(0xde), - ); - }); - - testWidgets('Filter chip check mark color is determined by platform brightness when light', (WidgetTester tester) async { - await _pumpCheckmarkChip( - tester, - chip: _selectedFilterChip(), - ); - - _expectCheckmarkColor( - find.byType(FilterChip), - Colors.black.withAlpha(0xde), - ); - }); - - testWidgets('Input chip check mark color is determined by platform brightness when dark', (WidgetTester tester) async { - await _pumpCheckmarkChip( - tester, - chip: _selectedInputChip(), - brightness: Brightness.dark, - ); - - _expectCheckmarkColor( - find.byType(InputChip), - Colors.white.withAlpha(0xde), - ); - }); - - testWidgets('Filter chip check mark color is determined by platform brightness when dark', (WidgetTester tester) async { - await _pumpCheckmarkChip( - tester, - chip: _selectedFilterChip(), - brightness: Brightness.dark, - ); - - _expectCheckmarkColor( - find.byType(FilterChip), - Colors.white.withAlpha(0xde), - ); - }); - - testWidgets('Input chip check mark color can be set by the chip theme', (WidgetTester tester) async { - await _pumpCheckmarkChip( - tester, - chip: _selectedInputChip(), - themeColor: const Color(0xff00ff00), - ); - - _expectCheckmarkColor( - find.byType(InputChip), - const Color(0xff00ff00), - ); - }); - - testWidgets('Filter chip check mark color can be set by the chip theme', (WidgetTester tester) async { - await _pumpCheckmarkChip( - tester, - chip: _selectedFilterChip(), - themeColor: const Color(0xff00ff00), - ); - - _expectCheckmarkColor( - find.byType(FilterChip), - const Color(0xff00ff00), - ); - }); - - testWidgets('Input chip check mark color can be set by the chip constructor', (WidgetTester tester) async { - await _pumpCheckmarkChip( - tester, - chip: _selectedInputChip(checkmarkColor: const Color(0xff00ff00)), - ); - - _expectCheckmarkColor( - find.byType(InputChip), - const Color(0xff00ff00), - ); - }); - - testWidgets('Filter chip check mark color can be set by the chip constructor', (WidgetTester tester) async { - await _pumpCheckmarkChip( - tester, - chip: _selectedFilterChip(checkmarkColor: const Color(0xff00ff00)), - ); - - _expectCheckmarkColor( - find.byType(FilterChip), - const Color(0xff00ff00), - ); - }); - - testWidgets('Input chip check mark color is set by chip constructor even when a theme color is specified', (WidgetTester tester) async { - await _pumpCheckmarkChip( - tester, - chip: _selectedInputChip(checkmarkColor: const Color(0xffff0000)), - themeColor: const Color(0xff00ff00), - ); - - _expectCheckmarkColor( - find.byType(InputChip), - const Color(0xffff0000), - ); - }); - - testWidgets('Filter chip check mark color is set by chip constructor even when a theme color is specified', (WidgetTester tester) async { - await _pumpCheckmarkChip( - tester, - chip: _selectedFilterChip(checkmarkColor: const Color(0xffff0000)), - themeColor: const Color(0xff00ff00), - ); - - _expectCheckmarkColor( - find.byType(FilterChip), - const Color(0xffff0000), - ); - }); - testWidgets('Chip delete button tooltip can be disabled using useDeleteButtonTooltip', (WidgetTester tester) async { await tester.pumpWidget( - _chipWithOptionalDeleteButton( + chipWithOptionalDeleteButton( deletable: true, useDeleteButtonTooltip: false, ), @@ -3522,7 +3151,7 @@ void main() { testWidgets('Chip delete button tooltip is disabled if deleteButtonTooltipMessage is empty', (WidgetTester tester) async { final UniqueKey deleteButtonKey = UniqueKey(); await tester.pumpWidget( - _chipWithOptionalDeleteButton( + chipWithOptionalDeleteButton( deleteButtonKey: deleteButtonKey, deletable: true, deleteButtonTooltipMessage: '', @@ -3547,7 +3176,7 @@ void main() { testWidgets('Disabling delete button tooltip does not disable chip tooltip', (WidgetTester tester) async { final UniqueKey deleteButtonKey = UniqueKey(); await tester.pumpWidget( - _chipWithOptionalDeleteButton( + chipWithOptionalDeleteButton( deleteButtonKey: deleteButtonKey, deletable: true, deleteButtonTooltipMessage: '', @@ -3575,7 +3204,7 @@ void main() { testWidgets('Triggering delete button tooltip does not trigger Chip tooltip', (WidgetTester tester) async { final UniqueKey deleteButtonKey = UniqueKey(); await tester.pumpWidget( - _chipWithOptionalDeleteButton( + chipWithOptionalDeleteButton( deleteButtonKey: deleteButtonKey, deletable: true, chipTooltip: 'Chip Tooltip', @@ -3601,7 +3230,7 @@ void main() { testWidgets('intrinsicHeight implementation meets constraints', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/49478. - await tester.pumpWidget(_wrapForChip( + await tester.pumpWidget(wrapForChip( child: const Chip( label: Text('text'), padding: EdgeInsets.symmetric(horizontal: 20),