From 0b796e1c5909b7dbec7f7bfa6498ffbc1b2021ce Mon Sep 17 00:00:00 2001 From: ahmed-osman3 <99483750+ahmed-osman3@users.noreply.github.com> Date: Wed, 21 Feb 2024 14:54:47 +0000 Subject: [PATCH] Add icon button (#26) * Add icon button * Update action to run on pull_request_target --------- Co-authored-by: Osman Co-authored-by: Luke --- .github/workflows/pull-request.yml | 5 +- .../lib/pages/components/button_example.dart | 43 ++++- lib/src/components/buttons/button.dart | 133 +-------------- lib/src/components/buttons/button_style.dart | 155 ++++++++++++++++++ lib/src/components/buttons/icon_button.dart | 145 ++++++++++++++++ lib/zeta_flutter.dart | 2 + 6 files changed, 351 insertions(+), 132 deletions(-) create mode 100644 lib/src/components/buttons/button_style.dart create mode 100644 lib/src/components/buttons/icon_button.dart diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 30591e87..1271db0b 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,5 +1,8 @@ name: CI - Pull Request -on: pull_request +on: + pull_request_target: + types: + - opened jobs: up-to-date: diff --git a/example/lib/pages/components/button_example.dart b/example/lib/pages/components/button_example.dart index c99d80e6..baf17840 100644 --- a/example/lib/pages/components/button_example.dart +++ b/example/lib/pages/components/button_example.dart @@ -102,10 +102,21 @@ class _ButtonExampleState extends State { Column(children: buttons(ZetaWidgetBorder.sharp)), Text('Full Buttons', style: ZetaTextStyles.displayMedium), Column(children: buttons(ZetaWidgetBorder.full)), - Text('Floating Action Buttons', style: ZetaTextStyles.displayMedium), - Text('Tap buttons to change current FAB: ', style: ZetaTextStyles.bodyMedium), - Wrap(children: fabs.divide(SizedBox.square(dimension: 10)).toList()), - ].divide(const SizedBox.square(dimension: ZetaSpacing.m)).toList(), + Text('Icon Buttons', style: ZetaTextStyles.displayLarge), + Text('Rounded Buttons', style: ZetaTextStyles.displayMedium), + Column(children: inputButtons(ZetaWidgetBorder.rounded)), + Text('Sharp Buttons', style: ZetaTextStyles.displayMedium), + Column(children: inputButtons(ZetaWidgetBorder.sharp)), + Text('Floating Action Buttons', + style: ZetaTextStyles.displayMedium), + Text('Tap buttons to change current FAB: ', + style: ZetaTextStyles.bodyMedium), + Wrap( + children: + fabs.divide(SizedBox.square(dimension: 10)).toList()), + ] + .divide(const SizedBox.square(dimension: ZetaSpacing.m)) + .toList(), ), ), Expanded(child: const SizedBox()), @@ -136,4 +147,28 @@ class _ButtonExampleState extends State { ), ).reversed.divide(const SizedBox.square(dimension: ZetaSpacing.m)).toList(); } + + List inputButtons(ZetaWidgetBorder borderType) { + return List.generate( + ZetaWidgetSize.values.length + 1, + (index) => SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, + children: List.generate( + ZetaButtonType.values.length, + (index2) => ZetaIconButton( + onPressed: index == 0 ? null : () {}, + type: ZetaButtonType.values[index2], + size: ZetaWidgetSize.values[index == 0 ? 0 : index - 1], + borderType: borderType, + icon: ZetaButtonType.values[index2] == ZetaButtonType.negative + ? ZetaIcons.delete_round + : ZetaIcons.more_horizontal_round, + ), + ).divide(const SizedBox.square(dimension: ZetaSpacing.m)).toList(), + ), + ), + ).reversed.divide(const SizedBox.square(dimension: ZetaSpacing.m)).toList(); + } } diff --git a/lib/src/components/buttons/button.dart b/lib/src/components/buttons/button.dart index 70ae5644..7c0091e6 100644 --- a/lib/src/components/buttons/button.dart +++ b/lib/src/components/buttons/button.dart @@ -4,71 +4,6 @@ import 'package:flutter/material.dart'; import '../../../zeta_flutter.dart'; ///Button types -enum ZetaButtonType { - /// Background: Primary color; defaults to blue. - /// Border: None. - primary, - - /// Background: Secondary color; defaults to yellow. - /// Border: None. - secondary, - - /// Background: Positive color; defaults to green. - /// Border: None. - positive, - - /// Background: Negative color; defaults to red. - /// Border: None. - negative, - - /// Background: None. - /// Border: Primary color; defaults to blue. - outline, - - /// Background: None. - /// Border: Subtle color; defaults to cool grey. - outlineSubtle, - - /// Background: None. - /// Border: None. - /// Foreground color: Primary; defaults to blue. - text, -} - -extension on ZetaButtonType { - ZetaColorSwatch color(ZetaColors colors) { - switch (this) { - case ZetaButtonType.secondary: - return colors.secondary; - case ZetaButtonType.positive: - return colors.positive; - case ZetaButtonType.negative: - return colors.negative; - case ZetaButtonType.outline: - case ZetaButtonType.primary: - return colors.primary; - case ZetaButtonType.outlineSubtle: - case ZetaButtonType.text: - return colors.cool; - } - } - - bool get border => this == ZetaButtonType.outline || this == ZetaButtonType.outlineSubtle; - bool get solid => index < 4; -} - -extension on ZetaWidgetBorder { - BorderRadius get radius { - switch (this) { - case ZetaWidgetBorder.sharp: - return ZetaRadius.none; - case ZetaWidgetBorder.rounded: - return ZetaRadius.minimal; - case ZetaWidgetBorder.full: - return ZetaRadius.full; - } - } -} ///Zeta Button class ZetaButton extends StatelessWidget { @@ -184,10 +119,11 @@ class ZetaButton extends StatelessWidget { Widget build(BuildContext context) { final colors = Zeta.of(context).colors; return ConstrainedBox( - constraints: BoxConstraints(minHeight: _minConstraints, minWidth: _minConstraints), + constraints: + BoxConstraints(minHeight: _minConstraints, minWidth: _minConstraints), child: FilledButton( onPressed: onPressed, - style: _buttonStyle(colors), + style: buttonStyle(colors, borderType, type, null), child: SelectionContainer.disabled( child: label.isEmpty ? const SizedBox() @@ -200,66 +136,9 @@ class ZetaButton extends StatelessWidget { ); } - ButtonStyle _buttonStyle(ZetaColors colors) { - return ButtonStyle( - minimumSize: MaterialStateProperty.all(const Size.square(32)), - shape: MaterialStateProperty.all(RoundedRectangleBorder(borderRadius: borderType.radius)), - backgroundColor: MaterialStateProperty.resolveWith( - (states) { - if (states.contains(MaterialState.disabled)) { - return colors.surfaceDisabled; - } - if (states.contains(MaterialState.pressed)) { - return type.solid ? type.color(colors).shade70 : colors.primary.shade10; - } - if (states.contains(MaterialState.hovered)) { - return type.solid ? type.color(colors).shade50 : colors.cool.shade20; - } - return type.solid ? type.color(colors) : Colors.transparent; - }, - ), - foregroundColor: MaterialStateProperty.resolveWith( - (states) { - if (states.contains(MaterialState.disabled)) { - return colors.textDisabled; - } - switch (type) { - case ZetaButtonType.outline: - case ZetaButtonType.text: - return colors.primary; - case ZetaButtonType.outlineSubtle: - return colors.textDefault; - case ZetaButtonType.primary: - case ZetaButtonType.secondary: - case ZetaButtonType.positive: - case ZetaButtonType.negative: - return type.color(colors).onColor; - } - }, - ), - overlayColor: MaterialStateProperty.resolveWith((Set states) { - return null; - }), - side: MaterialStateProperty.resolveWith((Set states) { - if (type.border && states.contains(MaterialState.disabled)) { - return BorderSide(color: colors.cool.shade40); - } - // TODO(thelukewalton): This removes a defualt border when focused, rather than adding a second border when focused. - if (states.contains(MaterialState.focused)) { - return BorderSide(color: colors.blue, width: ZetaSpacing.x0_5); - } - if (type.border) { - return BorderSide(color: type == ZetaButtonType.outline ? colors.primary.border : colors.borderDefault); - } - - return null; - }), - elevation: const MaterialStatePropertyAll(0), - padding: MaterialStateProperty.all(EdgeInsets.zero), - ); - } - - TextStyle get _textStyle => size == ZetaWidgetSize.small ? ZetaTextStyles.labelMedium : ZetaTextStyles.labelLarge; + TextStyle get _textStyle => size == ZetaWidgetSize.small + ? ZetaTextStyles.labelMedium + : ZetaTextStyles.labelLarge; double get _minConstraints { switch (size) { diff --git a/lib/src/components/buttons/button_style.dart b/lib/src/components/buttons/button_style.dart new file mode 100644 index 00000000..49d0341b --- /dev/null +++ b/lib/src/components/buttons/button_style.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; + +import '../../../zeta_flutter.dart'; + +/// Shared enum for type of buttons. +enum ZetaButtonType { + /// Background: Primary color; defaults to blue. + /// Border: None. + primary, + + /// Background: Secondary color; defaults to yellow. + /// Border: None. + secondary, + + /// Background: Positive color; defaults to green. + /// Border: None. + positive, + + /// Background: Negative color; defaults to red. + /// Border: None. + negative, + + /// Background: None. + /// Border: Primary color; defaults to blue. + outline, + + /// Background: None. + /// Border: Subtle color; defaults to cool grey. + outlineSubtle, + + /// Background: None. + /// Border: None. + /// Foreground color: Primary; defaults to blue. + text, +} + +/// Button utility functions for styling +extension ButtonFunctions on ZetaButtonType { + /// Returns color based on [ZetaButtonType] + ZetaColorSwatch color(ZetaColors colors) { + switch (this) { + case ZetaButtonType.secondary: + return colors.secondary; + case ZetaButtonType.positive: + return colors.positive; + case ZetaButtonType.negative: + return colors.negative; + case ZetaButtonType.outline: + case ZetaButtonType.primary: + return colors.primary; + case ZetaButtonType.outlineSubtle: + case ZetaButtonType.text: + return colors.cool; + } + } + + /// Returns if button has border + bool get border => + this == ZetaButtonType.outline || this == ZetaButtonType.outlineSubtle; + + ///Returns if button is solid + bool get solid => index < 4; +} + +extension on ZetaColors {} + +///Border utility functions +extension BorderFunctions on ZetaWidgetBorder { + ///Returns radius based on [ZetaWidgetBorder] + BorderRadius get radius { + switch (this) { + case ZetaWidgetBorder.sharp: + return ZetaRadius.none; + case ZetaWidgetBorder.rounded: + return ZetaRadius.minimal; + case ZetaWidgetBorder.full: + return ZetaRadius.full; + } + } +} + +/// Shared buttonStyle for buttons and icon buttons +ButtonStyle buttonStyle(ZetaColors colors, ZetaWidgetBorder borderType, + ZetaButtonType type, Color? backgroundColor) { + final ZetaColorSwatch color = backgroundColor != null + ? ZetaColorSwatch.fromColor(backgroundColor) + : type.color(colors); + + final bool isSolid = type.solid || backgroundColor != null; + + return ButtonStyle( + minimumSize: MaterialStateProperty.all(const Size.square(32)), + shape: MaterialStateProperty.all( + RoundedRectangleBorder(borderRadius: borderType.radius), + ), + backgroundColor: MaterialStateProperty.resolveWith( + (states) { + if (states.contains(MaterialState.disabled)) { + return colors.surfaceDisabled; + } + if (states.contains(MaterialState.pressed)) { + return isSolid ? color.shade70 : colors.primary.shade10; + } + if (states.contains(MaterialState.hovered)) { + return isSolid ? color.shade50 : colors.cool.shade20; + } + if (backgroundColor != null) return backgroundColor; + return isSolid ? color : Colors.transparent; + }, + ), + foregroundColor: MaterialStateProperty.resolveWith( + (states) { + if (states.contains(MaterialState.disabled)) { + return colors.textDisabled; + } + switch (type) { + case ZetaButtonType.outline: + case ZetaButtonType.text: + return colors.primary; + case ZetaButtonType.outlineSubtle: + return colors.textDefault; + case ZetaButtonType.primary: + case ZetaButtonType.secondary: + case ZetaButtonType.positive: + case ZetaButtonType.negative: + return color.onColor; + } + }, + ), + overlayColor: + MaterialStateProperty.resolveWith((Set states) { + return null; + }), + side: MaterialStateProperty.resolveWith((Set states) { + if (type.border && states.contains(MaterialState.disabled)) { + return BorderSide(color: colors.cool.shade40); + } + // TODO(thelukewalton): This removes a defualt border when focused, rather than adding a second border when focused. + if (states.contains(MaterialState.focused)) { + return BorderSide(color: colors.blue, width: ZetaSpacing.x0_5); + } + if (type.border) { + return BorderSide( + color: type == ZetaButtonType.outline + ? colors.primary.border + : colors.borderDefault, + ); + } + + return null; + }), + elevation: const MaterialStatePropertyAll(0), + padding: MaterialStateProperty.all(EdgeInsets.zero), + ); +} diff --git a/lib/src/components/buttons/icon_button.dart b/lib/src/components/buttons/icon_button.dart new file mode 100644 index 00000000..b19ed7a4 --- /dev/null +++ b/lib/src/components/buttons/icon_button.dart @@ -0,0 +1,145 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zeta_flutter.dart'; + +/// Component [ZetaIconButton] +class ZetaIconButton extends StatelessWidget { + /// Constructor for [ZetaIconButton] + const ZetaIconButton({ + super.key, + this.onPressed, + this.borderType = ZetaWidgetBorder.rounded, + this.type = ZetaButtonType.primary, + this.size = ZetaWidgetSize.medium, + this.icon = ZetaIcons.more_horizontal_round, + }); + + /// Constructs [ZetaIconButton] with Primary theme. + const ZetaIconButton.primary({ + required this.icon, + this.onPressed, + this.size = ZetaWidgetSize.medium, + this.borderType = ZetaWidgetBorder.rounded, + super.key, + }) : type = ZetaButtonType.primary; + + /// Constructs [ZetaIconButton] with Secondary theme. + const ZetaIconButton.secondary({ + required this.icon, + this.onPressed, + this.size = ZetaWidgetSize.medium, + this.borderType = ZetaWidgetBorder.rounded, + super.key, + }) : type = ZetaButtonType.secondary; + + /// Constructs [ZetaIconButton] with Positive theme. + const ZetaIconButton.positive({ + required this.icon, + this.onPressed, + this.size = ZetaWidgetSize.medium, + this.borderType = ZetaWidgetBorder.rounded, + super.key, + }) : type = ZetaButtonType.positive; + + /// Constructs [ZetaIconButton] with Negative theme. + const ZetaIconButton.negative({ + this.onPressed, + this.size = ZetaWidgetSize.medium, + this.borderType = ZetaWidgetBorder.rounded, + required this.icon, + super.key, + }) : type = ZetaButtonType.negative; + + /// Constructs [ZetaIconButton] with Outline theme. + const ZetaIconButton.outline({ + required this.icon, + this.onPressed, + this.size = ZetaWidgetSize.medium, + this.borderType = ZetaWidgetBorder.rounded, + super.key, + }) : type = ZetaButtonType.outline; + + /// Constructs [ZetaIconButton] with Outline Subtle theme. + const ZetaIconButton.outlineSubtle({ + required this.icon, + this.onPressed, + this.size = ZetaWidgetSize.medium, + this.borderType = ZetaWidgetBorder.rounded, + super.key, + }) : type = ZetaButtonType.outlineSubtle; + + /// Constructs [ZetaIconButton] with text theme. + const ZetaIconButton.text({ + required this.icon, + this.onPressed, + this.size = ZetaWidgetSize.medium, + this.borderType = ZetaWidgetBorder.rounded, + super.key, + }) : type = ZetaButtonType.text; + + ///Button label + final IconData icon; + + ///Called when the button is tapped or otherwise activated. + final VoidCallback? onPressed; + + ///The coloring type of the button + final ZetaButtonType type; + + ///Whether or not the button is sharp or rounded + ///Defaults to rounded + final ZetaWidgetBorder borderType; + + /// Size of the button. Defaults to large. + final ZetaWidgetSize size; + + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + + return FilledButton( + onPressed: onPressed, + style: buttonStyle(colors, borderType, type, null), + child: SelectionContainer.disabled( + child: Icon( + icon, + size: _iconSize, + ).paddingAll(_iconPadding), + ), + ); + } + + double get _iconPadding { + switch (size) { + case ZetaWidgetSize.large: + return ZetaSpacing.x3; + case ZetaWidgetSize.medium: + return ZetaSpacing.x2; + case ZetaWidgetSize.small: + return ZetaSpacing.x1; + } + } + + double get _iconSize { + switch (size) { + case ZetaWidgetSize.large: + return ZetaSpacing.x6; + case ZetaWidgetSize.medium: + return ZetaSpacing.x6; + case ZetaWidgetSize.small: + return ZetaSpacing.x5; + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('size', size)) + ..add(EnumProperty('borderType', borderType)) + ..add(DiagnosticsProperty('icon', icon)) + ..add(ObjectFlagProperty.has('onPressed', onPressed)) + ..add(EnumProperty('type', type)); + } +} diff --git a/lib/zeta_flutter.dart b/lib/zeta_flutter.dart index eb763dca..c968f8fc 100644 --- a/lib/zeta_flutter.dart +++ b/lib/zeta_flutter.dart @@ -15,7 +15,9 @@ export 'src/components/banners/system_banner.dart'; export 'src/components/bottom sheets/bottom_sheet.dart'; export 'src/components/bottom sheets/menu_items.dart'; export 'src/components/buttons/button.dart'; +export 'src/components/buttons/button_style.dart'; export 'src/components/buttons/fab.dart'; +export 'src/components/buttons/icon_button.dart'; export 'src/components/checkbox/checkbox.dart'; export 'src/components/chips/chip.dart'; export 'src/components/password/password_input.dart';