From ccb3fe9878bb59b8cd517f2bc98bb55d53bdb88e Mon Sep 17 00:00:00 2001 From: Luke Walton Date: Wed, 12 Jun 2024 12:04:09 +0100 Subject: [PATCH] refactor: chip (#96) --- .../lib/pages/components/chip_example.dart | 276 +++++++----------- example/macos/Podfile.lock | 2 +- lib/src/components/chips/assist_chip.dart | 14 +- lib/src/components/chips/chip.dart | 250 +++++++++------- lib/src/components/chips/filter_chip.dart | 24 +- lib/src/components/chips/input_chip.dart | 9 +- .../filter_selection/filter_selection.dart | 16 +- 7 files changed, 299 insertions(+), 292 deletions(-) diff --git a/example/lib/pages/components/chip_example.dart b/example/lib/pages/components/chip_example.dart index a5c8e5dd..8eb0222f 100644 --- a/example/lib/pages/components/chip_example.dart +++ b/example/lib/pages/components/chip_example.dart @@ -2,14 +2,19 @@ import 'package:flutter/material.dart'; import 'package:zeta_example/widgets.dart'; import 'package:zeta_flutter/zeta_flutter.dart'; -class ChipExample extends StatelessWidget { +class ChipExample extends StatefulWidget { static const String name = 'Chip'; - const ChipExample({super.key}); + @override + State createState() => _ChipExampleState(); +} + +class _ChipExampleState extends State { + String chipType = 'none'; @override Widget build(BuildContext context) { - final List inputChipExample = [ + final Widget inputChipExample = Column(children: [ Text( 'Input Chip', textAlign: TextAlign.center, @@ -17,210 +22,143 @@ class ChipExample extends StatelessWidget { ), const SizedBox(height: 10), Row( - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Column( - children: [ - Text( - 'Rounded', - textAlign: TextAlign.center, - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 30), - Text('Label Only', textAlign: TextAlign.center), - const SizedBox(height: 10), - ZetaInputChip(label: 'Label'), - const SizedBox(height: 30), - Text('Label + Icon', textAlign: TextAlign.center), - const SizedBox(height: 10), - ZetaInputChip( - label: 'Label', - ), - const SizedBox(height: 30), - Text('Label + Avatar', textAlign: TextAlign.center), - const SizedBox(height: 10), - ZetaInputChip( - label: 'Label', - leading: const Icon(ZetaIcons.user_round), - ), - const SizedBox(height: 30), - Text('Label, Avatar + Icon', textAlign: TextAlign.center), - const SizedBox(height: 10), - ZetaInputChip( - label: 'Label', - leading: const Icon(ZetaIcons.user_round), - trailing: Icon(ZetaIcons.close_round), - ), - ], + Expanded( + child: Column( + children: [ + ZetaInputChip( + label: 'Label', + leading: ZetaAvatar.initials(initials: "ZA"), + trailing: IconButton(icon: Icon(ZetaIcons.close_round), onPressed: () {}), + ), + ], + ), ), - Column( - children: [ - Text( - 'Sharp', - textAlign: TextAlign.center, - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 30), - Text('Label Only', textAlign: TextAlign.center), - const SizedBox(height: 10), - ZetaInputChip( - label: 'Label', - rounded: false, - ), - const SizedBox(height: 30), - Text('Label + Icon', textAlign: TextAlign.center), - const SizedBox(height: 10), - ZetaInputChip( - label: 'Label', - rounded: false, - ), - const SizedBox(height: 30), - Text('Label + Avatar', textAlign: TextAlign.center), - const SizedBox(height: 10), - ZetaInputChip( - label: 'Label', - rounded: false, - leading: const Icon(ZetaIcons.user_round), - ), - const SizedBox(height: 30), - Text('Label, Avatar + Icon', textAlign: TextAlign.center), - const SizedBox(height: 10), - ZetaInputChip( - label: 'Label', - rounded: false, - leading: const Icon(ZetaIcons.user_round), - trailing: Icon(ZetaIcons.close_sharp), - ), - ], + Expanded( + child: Column( + children: [ + ZetaInputChip( + label: 'Label', + rounded: false, + leading: const Icon(ZetaIcons.user_round), + trailing: Icon(ZetaIcons.close_sharp), + ), + ], + ), ), ], ), - ]; + ]); - final List filterChipExample = [ + final Widget assistChipExample = Column(children: [ Text( - 'Filter Chip', + 'Assist Chip', textAlign: TextAlign.center, style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 10), Row( - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Column( - children: [ - Text( - 'Rounded', - textAlign: TextAlign.center, - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 10), - ZetaFilterChip(label: 'Label'), - const SizedBox(height: 10), - ZetaFilterChip( - label: 'Label', - selected: true, - ), - ], + Expanded( + child: Column( + children: [ + ZetaAssistChip( + label: 'Label', + leading: Icon(ZetaIcons.star_round), + draggable: true, + data: 'Round Assist chip', + ), + ], + ), ), - Column( - children: [ - Text( - 'Sharp', - textAlign: TextAlign.center, - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 10), - ZetaFilterChip( - label: 'Label', - rounded: false, - ), - const SizedBox(height: 10), - ZetaFilterChip( - label: 'Label', - rounded: false, - selected: true, - ), - ], + Expanded( + child: Column( + children: [ + ZetaAssistChip( + label: 'Label', + rounded: false, + leading: Icon(ZetaIcons.star_round), + data: 'Sharp Assist chip', + draggable: true, + ), + ], + ), ), ], ), - ]; + ]); - final List assistChipExample = [ + final Widget filterChipExample = Column(children: [ Text( - 'Assist Chip', + 'Filter Chip', textAlign: TextAlign.center, style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 10), Row( - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Column( - children: [ - Text( - 'Rounded', - textAlign: TextAlign.center, - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 30), - Text('Label Only', textAlign: TextAlign.center), - const SizedBox(height: 10), - ZetaAssistChip(label: 'Label'), - const SizedBox(height: 30), - Text('Label + Icon', textAlign: TextAlign.center), - const SizedBox(height: 10), - ZetaAssistChip( - label: 'Label', - leading: Icon(ZetaIcons.star_round), - ), - ], + Expanded( + child: Column( + children: [ + ZetaFilterChip( + label: 'Label', + selected: true, + data: 'Round filter chip', + draggable: true, + ), + ], + ), ), - Column( - children: [ - Text( - 'Sharp', - textAlign: TextAlign.center, - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 30), - Text('Label Only', textAlign: TextAlign.center), - const SizedBox(height: 10), - ZetaAssistChip( - label: 'Label', - rounded: false, - ), - const SizedBox(height: 30), - Text('Label + Icon', textAlign: TextAlign.center), - const SizedBox(height: 10), - ZetaAssistChip( - label: 'Label', - rounded: false, - leading: Icon(ZetaIcons.star_round), - ), - ], + Expanded( + child: Column( + children: [ + ZetaFilterChip( + label: 'Label', + rounded: false, + selected: true, + data: 'Sharp filter chip', + draggable: true, + ), + ], + ), ), ], ), - ]; - + ]); + final colors = Zeta.of(context).colors; return ExampleScaffold( name: ChipExample.name, child: SingleChildScrollView( padding: EdgeInsets.all(ZetaSpacing.medium), child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - ...inputChipExample, - const SizedBox(height: 30), - ...assistChipExample, - const SizedBox(height: 30), - ...filterChipExample, - const SizedBox(height: 30), - ], + Row( + children: [ + Expanded(child: Center(child: Text('Rounded'))), + Expanded(child: Center(child: Text('Sharp'))), + ], + ), + inputChipExample, + assistChipExample, + filterChipExample, + const SizedBox(height: 100), + DragTarget( + onAcceptWithDetails: (details) => setState(() => chipType = details.data), + builder: (context, _, __) { + return Container( + padding: EdgeInsets.all(ZetaSpacing.medium), + color: colors.surfaceSelectedHover, + height: 100, + width: 200, + child: Center(child: Text('Last chip dragged here: $chipType')), + ); + }, + ) + ].gap(30), ), ), ); diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index 66e9c6e8..4d2c877c 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -33,7 +33,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 diff --git a/lib/src/components/chips/assist_chip.dart b/lib/src/components/chips/assist_chip.dart index 4dc7658d..58e93c58 100644 --- a/lib/src/components/chips/assist_chip.dart +++ b/lib/src/components/chips/assist_chip.dart @@ -1,7 +1,13 @@ -import 'chip.dart'; +import 'package:flutter/material.dart'; + +import '../../../zeta_flutter.dart'; /// Zeta Assist Chip. /// +/// Leading widget should typically be an icon. +/// +/// These chips use [Draggable] and can be dragged around the screen and placed in new locations using [DragTarget]. +/// /// Extends [ZetaChip]. class ZetaAssistChip extends ZetaChip { /// Creates a [ZetaAssistChip]. @@ -10,5 +16,9 @@ class ZetaAssistChip extends ZetaChip { required super.label, super.leading, super.rounded, - }) : super(type: ZetaChipType.assist); + super.draggable = false, + super.data, + super.onDragCompleted, + super.onTap, + }); } diff --git a/lib/src/components/chips/chip.dart b/lib/src/components/chips/chip.dart index 90f3150e..33c63c16 100644 --- a/lib/src/components/chips/chip.dart +++ b/lib/src/components/chips/chip.dart @@ -7,41 +7,31 @@ export './assist_chip.dart'; export './filter_chip.dart'; export './input_chip.dart'; -/// The type of [ZetaChip] -enum ZetaChipType { - /// Input Chip - input, - - /// Filter Chip - filter, - - /// Assist Chip - assist, -} - /// Zeta Chip component. /// -/// This covers the board functionality of [ZetaAssistChip], [ZetaFilterChip] and [ZetaInputChip]. +/// This covers the broad functionality of [ZetaAssistChip], [ZetaFilterChip] and [ZetaInputChip]. +/// +/// If [selected] is not null, the chip will have the toggle behavior of [ZetaFilterChip]. class ZetaChip extends StatefulWidget { /// Constructs a [ZetaChip]. const ZetaChip({ super.key, required this.label, - required this.type, this.leading, this.rounded = true, this.trailing, this.selected, this.onTap, + this.draggable = false, + this.data, + this.onDragCompleted, + this.onToggle, }); - /// Type of [Chip]. - final ZetaChipType type; - /// The label on the [ZetaChip] final String label; - /// Leading component. Typically an [Icon]. + /// Leading component. Typically an [Icon] or [ZetaAvatar]. final Widget? leading; /// {@macro zeta-component-rounded} @@ -51,10 +41,28 @@ class ZetaChip extends StatefulWidget { final Widget? trailing; /// Whether the [ZetaFilterChip] is selected. + /// + /// If null, chip can not be selected. final bool? selected; /// Callback when chip is tapped. - final ValueSetter? onTap; + final VoidCallback? onTap; + + /// Callback for when Filter Chip is toggled. + final ValueSetter? onToggle; + + /// Whether the chip can be dragged. + final bool draggable; + + /// Draggable data. + final dynamic data; + + /// Called when the draggable is dropped and accepted by a [DragTarget]. + /// + /// See also: + /// * [DragTarget] + /// * [Draggable] + final VoidCallback? onDragCompleted; @override State createState() => _ZetaChipState(); @@ -62,21 +70,20 @@ class ZetaChip extends StatefulWidget { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(EnumProperty('type', type)) ..add(StringProperty('label', label)) ..add(DiagnosticsProperty('rounded', rounded)) ..add(DiagnosticsProperty('selected', selected)) - ..add(ObjectFlagProperty?>.has('onTap', onTap)); + ..add(DiagnosticsProperty('draggable', draggable)) + ..add(DiagnosticsProperty('data', data)) + ..add(ObjectFlagProperty.has('onDragCompleted', onDragCompleted)) + ..add(ObjectFlagProperty.has('onTap', onTap)) + ..add(ObjectFlagProperty>.has('onToggle', onToggle)); } } class _ZetaChipState extends State { bool selected = false; - Widget? get leading => - widget.leading ?? - (selected ? Icon(widget.rounded ? ZetaIcons.check_mark_round : ZetaIcons.check_mark_sharp) : null); - @override void initState() { super.initState(); @@ -84,94 +91,128 @@ class _ZetaChipState extends State { } Widget _renderLeading(Color foregroundColor) { - if (leading.runtimeType == Icon) { - return IconTheme( - data: IconThemeData( - color: foregroundColor, - size: ZetaSpacing.xl_1, - ), - child: leading!, - ); - } else if (leading.runtimeType == ZetaAvatar) { - return (leading! as ZetaAvatar).copyWith(size: ZetaAvatarSize.xxxs); + if (widget.leading.runtimeType == Icon) { + return IconTheme(data: IconThemeData(color: foregroundColor, size: ZetaSpacing.xl_1), child: widget.leading!); + } else if (widget.leading.runtimeType == ZetaAvatar) { + return (widget.leading! as ZetaAvatar).copyWith(size: ZetaAvatarSize.xxxs); } - return leading!; + return widget.leading ?? const SizedBox(); } + final _controller = WidgetStatesController(); + @override Widget build(BuildContext context) { final colors = Zeta.of(context).colors; final foregroundColor = selected ? colors.textInverse : colors.textDefault; - return FilledButton( - onPressed: () { - if (widget.type == ZetaChipType.filter) { - setState(() => selected = !selected); - widget.onTap?.call(selected); - } - }, - style: ButtonStyle( - shape: WidgetStateProperty.all( - RoundedRectangleBorder(borderRadius: widget.rounded ? ZetaRadius.full : ZetaRadius.none), - ), - textStyle: WidgetStateProperty.all(ZetaTextStyles.bodySmall), - backgroundColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.disabled)) { - return colors.surfaceDisabled; - } - if (selected) { - return colors.cool.shade90; - } - if (states.contains(WidgetState.pressed) || states.contains(WidgetState.dragged)) { - return colors.surfaceSelected; - } - - if (states.contains(WidgetState.hovered)) { - return colors.surfaceHover; - } - - return colors.surfacePrimary; - }), - foregroundColor: WidgetStateProperty.all(foregroundColor), - mouseCursor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.disabled)) { - return SystemMouseCursors.forbidden; - } - if (states.contains(WidgetState.dragged)) { - return SystemMouseCursors.grabbing; - } - return SystemMouseCursors.click; - }), - elevation: WidgetStateProperty.all(0), - side: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.focused)) { - return BorderSide(width: ZetaSpacingBase.x0_5, color: colors.blue.shade50); - } - return BorderSide(color: colors.borderDefault); - }), - padding: WidgetStateProperty.all( - EdgeInsets.fromLTRB( - widget.leading != null ? ZetaSpacingBase.x2_5 : ZetaSpacing.medium, - ZetaSpacing.none, - widget.trailing != null ? ZetaSpacingBase.x2_5 : ZetaSpacing.medium, - ZetaSpacing.none, - ), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (leading != null) _renderLeading(foregroundColor), - Text(widget.label), - if (widget.trailing != null) - IconTheme( - data: IconThemeData( - color: foregroundColor, - size: ZetaSpacing.xl_1, + + return SelectionContainer.disabled( + child: widget.draggable + ? Draggable( + feedback: Material( + color: Colors.transparent, + child: child(colors, foregroundColor, isDragging: true), ), - child: widget.trailing!, + childWhenDragging: const SizedBox(), + data: widget.data, + onDragCompleted: widget.onDragCompleted, + child: child(colors, foregroundColor), + ) + : child(colors, foregroundColor), + ); + } + + ValueListenableBuilder> child(ZetaColors colors, Color foregroundColor, {bool isDragging = false}) { + return ValueListenableBuilder( + valueListenable: _controller, + builder: (context, states, child) { + final double iconSize = selected ? ZetaSpacing.xl_2 : ZetaSpacing.none; + + return InkWell( + statesController: _controller, + borderRadius: widget.rounded ? ZetaRadius.full : ZetaRadius.none, + onTap: () { + if (widget.selected != null) { + setState(() => selected = !selected); + widget.onToggle?.call(selected); + } else { + widget.onTap?.call(); + } + }, + child: AnimatedContainer( + duration: Durations.short3, + height: ZetaSpacing.xl_5, + padding: EdgeInsets.fromLTRB( + widget.leading != null ? ZetaSpacingBase.x2_5 : ZetaSpacing.medium, + 0, + widget.trailing != null ? ZetaSpacingBase.x2_5 : ZetaSpacing.medium, + 0, ), - ].divide(const SizedBox.square(dimension: ZetaSpacing.small)).toList(), - ), + decoration: BoxDecoration( + color: () { + if (states.contains(WidgetState.disabled)) { + return colors.surfaceDisabled; + } + if (selected) { + if (states.contains(WidgetState.hovered)) { + return colors.borderHover; + } + return colors.surfaceDefaultInverse; + } + if (states.contains(WidgetState.pressed) || isDragging) { + return colors.surfaceSelected; + } + if (states.contains(WidgetState.hovered)) { + return colors.surfaceHover; + } + return colors.surfacePrimary; + }(), + borderRadius: widget.rounded ? ZetaRadius.full : ZetaRadius.none, + border: Border.fromBorderSide( + BorderSide( + color: _controller.value.contains(WidgetState.focused) ? colors.blue.shade50 : colors.borderDefault, + width: _controller.value.contains(WidgetState.focused) + ? ZetaSpacingBase.x0_5 + : !selected + ? 1 + : 0, + ), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.selected != null) + AnimatedContainer( + duration: Durations.short1, + width: iconSize, + child: (selected + ? Icon( + widget.rounded ? ZetaIcons.check_mark_round : ZetaIcons.check_mark_sharp, + color: widget.selected! ? colors.iconInverse : Colors.transparent, + ) + : const SizedBox()), + ) + else if (widget.leading != null) + _renderLeading(foregroundColor), + if ((widget.selected != null && selected) || widget.leading != null) + const SizedBox.square(dimension: ZetaSpacing.small), + Text( + widget.label, + style: ZetaTextStyles.bodySmall.apply(color: foregroundColor), + ), + if (widget.trailing != null) ...[ + const SizedBox.square(dimension: ZetaSpacing.small), + IconTheme( + data: IconThemeData(color: foregroundColor, size: ZetaSpacing.xl_1), + child: widget.trailing!, + ), + ], + ], + ), + ), + ); + }, ); } @@ -181,7 +222,6 @@ class _ZetaChipState extends State { properties ..add(DiagnosticsProperty('rounded', widget.rounded)) ..add(StringProperty('label', widget.label)) - ..add(EnumProperty('type', widget.type)) ..add(DiagnosticsProperty('selected', widget.selected)); } } diff --git a/lib/src/components/chips/filter_chip.dart b/lib/src/components/chips/filter_chip.dart index 7fa98fe1..712bbfe3 100644 --- a/lib/src/components/chips/filter_chip.dart +++ b/lib/src/components/chips/filter_chip.dart @@ -1,17 +1,26 @@ -import 'chip.dart'; +import 'package:flutter/material.dart'; -/// Zeta Filter Chip. +import '../../../zeta_flutter.dart'; + +/// Filter chips have 2 togglable states, representing selected and not selected. +/// +/// The chips are commonly used within a [ZetaFilterSelection]. +/// +/// These chips use [Draggable] and can be dragged around the screen and placed in new locations using [DragTarget]. /// /// Extends [ZetaChip]. class ZetaFilterChip extends ZetaChip { - /// Creates a [ZetaInputChip]. + /// Creates a [ZetaFilterChip]. const ZetaFilterChip({ super.key, required super.label, super.rounded, super.selected, - super.onTap, - }) : super(type: ZetaChipType.filter); + super.draggable = false, + super.data, + super.onDragCompleted, + ValueSetter? onTap, + }) : super(onToggle: onTap); /// Creates another instance of [ZetaFilterChip]. ZetaFilterChip copyWith({ @@ -21,7 +30,10 @@ class ZetaFilterChip extends ZetaChip { label: label, selected: selected, rounded: rounded ?? this.rounded, - onTap: onTap, + draggable: draggable, + data: data, + onDragCompleted: onDragCompleted, + onTap: onToggle, ); } } diff --git a/lib/src/components/chips/input_chip.dart b/lib/src/components/chips/input_chip.dart index 896af5b3..5fe66318 100644 --- a/lib/src/components/chips/input_chip.dart +++ b/lib/src/components/chips/input_chip.dart @@ -1,6 +1,8 @@ -import 'chip.dart'; +import '../../../zeta_flutter.dart'; -/// Zeta Input Chip. +/// Zeta Input Chip typically is used to associate some content or action with a user. +/// +/// Leading widget should typically be a [ZetaAvatar]. /// /// Extends [ZetaChip]. class ZetaInputChip extends ZetaChip { @@ -11,5 +13,6 @@ class ZetaInputChip extends ZetaChip { super.leading, super.rounded, super.trailing, - }) : super(type: ZetaChipType.input); + super.onTap, + }); } diff --git a/lib/src/components/filter_selection/filter_selection.dart b/lib/src/components/filter_selection/filter_selection.dart index 8241799c..b9eb3d53 100644 --- a/lib/src/components/filter_selection/filter_selection.dart +++ b/lib/src/components/filter_selection/filter_selection.dart @@ -28,12 +28,16 @@ class ZetaFilterSelection extends StatelessWidget { height: ZetaSpacing.xl_7, child: Row( children: [ - IconButton( - visualDensity: VisualDensity.compact, - onPressed: onPressed, - icon: Icon( - rounded ? ZetaIcons.filter_round : ZetaIcons.filter_sharp, - size: ZetaSpacing.xl_2, + Container( + height: ZetaSpacing.xl_7, + color: Zeta.of(context).colors.surfaceDefault, + child: IconButton( + visualDensity: VisualDensity.compact, + onPressed: onPressed, + icon: Icon( + rounded ? ZetaIcons.filter_round : ZetaIcons.filter_sharp, + size: ZetaSpacing.xl_2, + ), ), ), Expanded(