diff --git a/example/lib/home.dart b/example/lib/home.dart index ad1381d3..d455a251 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:zeta_example/pages/color_example.dart'; +import 'package:zeta_example/pages/checkbox_example.dart'; import 'package:zeta_example/pages/grid_example.dart'; import 'package:zeta_example/pages/spacing_example.dart'; import 'package:zeta_example/pages/typography_example.dart'; @@ -19,6 +20,8 @@ final List components = [ Component(SpacingExample.name, (context) => const SpacingExample()), Component(TypographyExample.name, (context) => const TypographyExample()), Component(ColorExample.name, (context) => const ColorExample()), + Component(CheckBoxExample.name, (context) => const CheckBoxExample()), + ]; class Home extends StatefulWidget { diff --git a/example/lib/pages/checkbox_example.dart b/example/lib/pages/checkbox_example.dart new file mode 100644 index 00000000..37ab826d --- /dev/null +++ b/example/lib/pages/checkbox_example.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../widgets.dart'; + +class CheckBoxExample extends StatefulWidget { + static const String name = 'Checkbox'; + + const CheckBoxExample({Key? key}) : super(key: key); + + @override + State createState() => _CheckBoxExampleState(); +} + +class _CheckBoxExampleState extends State { + bool? isChecked = true; + bool isEnabled = true; + + @override + Widget build(BuildContext context) { + return ExampleScaffold( + name: 'Checkbox', + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ZetaCheckbox( + value: isChecked, + isEnabled: isEnabled, + onChanged: (value) => setState(() => isChecked = value)), + ElevatedButton( + child: const Text('Disable'), + onPressed: () => setState(() => isEnabled = !isEnabled), + ) + ], + ), + Row(children: [const Text('Sharp Checkbox Enabled')]), + getCheckBoxRow(isEnabled: true), + Row(children: [const Text('Sharp Checkbox Disabled')]), + getCheckBoxRow(isEnabled: false), + Row(children: [const Text('Rounded Checkbox Enabled')]), + getCheckBoxRow(isEnabled: true, isSharp: false), + Row(children: [const Text('Rounded Checkbox Disabled')]), + getCheckBoxRow(isEnabled: false, isSharp: false), + ], + ), + ), + ); + } +} + +Row getCheckBoxRow({required bool isEnabled, bool isSharp = true}) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ZetaCheckbox( + value: true, + isEnabled: isEnabled, + label: 'Selected', + borderType: isSharp ? BorderType.sharp : BorderType.rounded, + onChanged: (value) => {}), + ZetaCheckbox( + value: false, + isEnabled: isEnabled, + label: 'Indeterminate', + borderType: isSharp ? BorderType.sharp : BorderType.rounded, + onChanged: (value) => {}), + ZetaCheckbox( + value: null, + borderType: isSharp ? BorderType.sharp : BorderType.rounded, + isEnabled: isEnabled, + onChanged: (value) => {}), + ]); +} diff --git a/example/test/checkbox_test.dart b/example/test/checkbox_test.dart new file mode 100644 index 00000000..abc531a6 --- /dev/null +++ b/example/test/checkbox_test.dart @@ -0,0 +1,73 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; +import 'package:flutter/material.dart'; + +void main() { + group('ZetaCheckbox Tests', () { + testWidgets('Initializes with correct parameters', + (WidgetTester tester) async { + await tester.pumpWidget( + TestWidgetCB( + widget: ZetaCheckbox( + value: true, + onChanged: (value) {}, + borderType: BorderType.rounded, + label: 'Test Checkbox', + checkboxSize: Size(30, 30), + )), + ); + + final checkboxFinder = find.byType(ZetaCheckbox); + final ZetaCheckbox checkbox = tester.firstWidget(checkboxFinder); + + expect(checkbox.value, true); + expect(checkbox.borderType, BorderType.rounded); + expect(checkbox.label, 'Test Checkbox'); + expect(checkbox.checkboxSize, Size(30, 30)); + }); + + testWidgets('ZetaCheckbox changes state on tap', (WidgetTester tester) async { + bool? checkboxValue = true; + + await tester.pumpWidget( + TestWidgetCB( + widget: ZetaCheckbox( + value: checkboxValue, + onChanged: (value) { + checkboxValue = value; + }, + )), + ); + + await tester.tap(find.byType(ZetaCheckbox)); + await tester.pump(); + + expect(checkboxValue, false); + }); + }); +} + +class TestWidgetCB extends StatelessWidget { + final Widget widget; + + const TestWidgetCB({Key? key, required this.widget}); + + @override + Widget build(BuildContext context) { + return ZetaProvider( + builder: (context, theme, __) { + return Builder(builder: (context) { + return MaterialApp( + theme: ThemeData( + fontFamily: theme.fontFamily, + textTheme: ZetaText.textTheme, + ), + home: Scaffold( + body: widget, + ), + ); + }); + }, + ); + } +} diff --git a/example/widgetbook/components/checkbox_widgetbook.dart b/example/widgetbook/components/checkbox_widgetbook.dart new file mode 100644 index 00000000..6909ee4e --- /dev/null +++ b/example/widgetbook/components/checkbox_widgetbook.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:zeta_example/pages/checkbox_example.dart'; + +WidgetbookComponent checkboxWidgetBook() { + return WidgetbookComponent( + name: 'Checkbox', + useCases: [ + WidgetbookUseCase( + name: 'Checkbox (sharp)', + builder: (context) => SingleChildScrollView( + child: Padding( + padding: EdgeInsets.only(top: 10), + child: getCheckBoxRow(isEnabled: true), + ), + ), + ), + WidgetbookUseCase( + name: 'Checkbox (rounded)', + builder: (context) => SingleChildScrollView( + child: Padding( + padding: EdgeInsets.only(top: 10), + child: getCheckBoxRow(isEnabled: true, isSharp: false), + ), + ), + ), + WidgetbookUseCase( + name: 'Checkbox disabled (rounded)', + builder: (context) => SingleChildScrollView( + child: Padding( + padding: EdgeInsets.only(top: 10), + child: getCheckBoxRow(isEnabled: false, isSharp: false), + ), + ), + ), + ], + ); +} diff --git a/example/widgetbook/widgetbook.dart b/example/widgetbook/widgetbook.dart index e1c128e3..df609965 100644 --- a/example/widgetbook/widgetbook.dart +++ b/example/widgetbook/widgetbook.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:widgetbook/widgetbook.dart'; import 'package:zeta_flutter/zeta_flutter.dart'; +import 'components/checkbox_widgetbook.dart'; import 'components/color_widgetbook.dart'; import 'components/grid_widgetbook.dart'; import 'components/spacing_widgetbook.dart'; @@ -24,6 +25,7 @@ class HotReload extends StatelessWidget { spacingWidgetbook(), textWidgetBook(), colorWidgetBook(), + checkboxWidgetBook() ], ), ], diff --git a/lib/src/components/checkbox.dart b/lib/src/components/checkbox.dart new file mode 100644 index 00000000..10a4c92b --- /dev/null +++ b/lib/src/components/checkbox.dart @@ -0,0 +1,201 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import '../../zeta_flutter.dart'; + +/// Zeta Checkbox border type +enum BorderType { + ///sharp border + sharp, + + ///rounded border + rounded, +} + +///Zeta Checkbox +class ZetaCheckbox extends StatelessWidget { + /// Constructs a [ZetaCheckbox]. + const ZetaCheckbox({ + required this.value, + required this.onChanged, + this.borderType = BorderType.sharp, + this.label, + this.labelStyle, + this.checkboxSize = const Size(25, 25), + this.selectedColor, + this.unselectedColor, + this.unselectedBorderColor, + this.unselectedBorderWidth = 2.5, + this.disabledColor, + this.isEnabled = true, + this.iconSize = 18.0, + super.key, + }) : assert(iconSize > 0, 'Icon size must be greater than 0'); + + /// Whether the checkbox is selected, unselected or null (indeterminate) + final bool? value; + + /// Called when the value of the checkbox should change. + final ValueChanged onChanged; + + /// The type of border to display + /// + /// defaults to sharp + final BorderType borderType; + + /// The label displayed next to the checkbox + final String? label; + + /// Style to use on the label + final TextStyle? labelStyle; + + ///Size of the checkbox + final Size checkboxSize; + + /// The color to use when this checkbox is checked. + final Color? selectedColor; + + ///Color of the checkbox when it's not selected + final Color? unselectedColor; + + ///Color of the border when the checkbox not selected + final Color? unselectedBorderColor; + + ///Width of the border when the checkbox is not selected + /// + /// Defaults to 2.5 + final double unselectedBorderWidth; + + ///Size of the icon displayed inside the checkbox + final double iconSize; + + ///If checkbox is enabled + /// + ///defaults to true + final bool isEnabled; + + ///Color of the checkbox when it's disabled + final Color? disabledColor; + + @override + Widget build(BuildContext context) { + final theme = Zeta.of(context); + final ValueNotifier isFocused = ValueNotifier(false); + + return FocusableActionDetector( + onFocusChange: (bool focus) => isFocused.value = focus, + child: GestureDetector( + onTap: _handleOnTap, + child: _buildCheckboxRow(theme, isFocused), + ), + ); + } + + void _handleOnTap() { + if (isEnabled) { + onChanged( + value == null + ? true + : value! + ? false + : null, + ); + } + } + + Widget _buildCheckboxRow( + Zeta theme, + ValueNotifier isFocused, + ) { + return Flex( + direction: Axis.horizontal, + mainAxisSize: MainAxisSize.min, + children: [ + _buildCheckboxContainer(theme, isFocused), + if (label != null) ...[ + Flexible( + child: Padding( + padding: const EdgeInsets.only(left: Dimensions.s), + child: Text( + label!, + style: labelStyle ?? ZetaText.zetaTitleMedium, + ), + ), + ), + ], + ], + ); + } + + Widget _buildCheckboxContainer(Zeta theme, ValueNotifier isFocused) { + return ValueListenableBuilder( + valueListenable: isFocused, + builder: (context, focused, child) { + return Container( + decoration: BoxDecoration( + boxShadow: _getBoxShadow(theme, focused), + color: _getCheckboxBackgroundColor(theme), + border: Border.all( + color: _getCheckboxBorderColor(theme), + width: unselectedBorderWidth, + ), + borderRadius: BorderRadius.circular( + borderType == BorderType.rounded ? 4.0 : 0.0, + ), + ), + width: checkboxSize.width, + height: checkboxSize.height, + child: _getCheckboxIcon(theme), + ); + }, + ); + } + + List _getBoxShadow(Zeta theme, bool focused) { + if (!focused) return []; + return [ + BoxShadow( + spreadRadius: 3, + color: theme.colors.surfaceSelectedHovered, + ), + ]; + } + + Widget _getCheckboxIcon(Zeta theme) { + if (value == null) return const SizedBox.shrink(); + return Icon( + value! ? Icons.check : Icons.remove, + color: isEnabled ? theme.colors.white : theme.colors.textDisabled, + size: iconSize, + ); + } + + Color _getCheckboxBackgroundColor(Zeta theme) { + if (!isEnabled) return disabledColor ?? theme.colors.surfaceDisabled; + if (value == null) return unselectedColor ?? theme.colors.surfaceSecondary; + return selectedColor ?? theme.colors.primary; + } + + Color _getCheckboxBorderColor(Zeta theme) { + if (!isEnabled || value != null) return Colors.transparent; + return unselectedBorderColor ?? theme.colors.borderDefault; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('value', value)) + ..add(ObjectFlagProperty>.has('onChanged', onChanged)) + ..add(EnumProperty('borderType', borderType)) + ..add(StringProperty('label', label)) + ..add(DiagnosticsProperty('labelStyle', labelStyle)) + ..add(DiagnosticsProperty('checkboxSize', checkboxSize)) + ..add(ColorProperty('selectedColor', selectedColor)) + ..add(ColorProperty('unselectedColor', unselectedColor)) + ..add(ColorProperty('unselectedBorderColor', unselectedBorderColor)) + ..add(DoubleProperty('unselectedBorderWidth', unselectedBorderWidth)) + ..add(ColorProperty('disabledColor', disabledColor)) + ..add(DiagnosticsProperty('isEnabled', isEnabled)) + ..add(DoubleProperty('iconSize', iconSize)); + } +} diff --git a/lib/zeta_flutter.dart b/lib/zeta_flutter.dart index 576d2887..f4babe3b 100644 --- a/lib/zeta_flutter.dart +++ b/lib/zeta_flutter.dart @@ -1,6 +1,7 @@ /// Zebra Design System (Zeta) - Flutter Component Library library zeta_flutter; +export 'src/components/checkbox.dart'; export 'src/components/grid.dart'; export 'src/components/spacing.dart'; export 'src/components/text.dart';