From 6ac8c4deb449e247cc2dddee4fa98b921c9a16d9 Mon Sep 17 00:00:00 2001 From: mikecoomber <58986130+mikecoomber@users.noreply.github.com> Date: Mon, 10 Jun 2024 10:40:45 +0100 Subject: [PATCH] feat: Created Select input (#89) refactor: Select input now wraps a ZetaTextInput feat: Dropdown now has a builder function to allow building custom children fix: Select input menu appears in the correct position below the input --- .../components/select_input_example.dart | 136 ++-- .../pages/components/text_input_example.dart | 14 +- .../components/select_input_widgetbook.dart | 91 +-- .../components/buttons/input_icon_button.dart | 73 ++ lib/src/components/date_input/date_input.dart | 66 +- lib/src/components/dropdown/dropdown.dart | 167 ++++- .../components/select_input/select_input.dart | 700 +++--------------- lib/src/components/text_input/text_input.dart | 12 +- lib/src/components/time_input/time_input.dart | 66 +- 9 files changed, 438 insertions(+), 887 deletions(-) create mode 100644 lib/src/components/buttons/input_icon_button.dart diff --git a/example/lib/pages/components/select_input_example.dart b/example/lib/pages/components/select_input_example.dart index d8f85c81..e7c145e2 100644 --- a/example/lib/pages/components/select_input_example.dart +++ b/example/lib/pages/components/select_input_example.dart @@ -11,14 +11,23 @@ class SelectInputExample extends StatefulWidget { } class _SelectInputExampleState extends State { - String? _errorText; - ZetaSelectInputItem? selectedItem = ZetaSelectInputItem( - value: 'Item 1', - ); + final formKey = GlobalKey(); @override Widget build(BuildContext context) { - final zeta = Zeta.of(context); + final items = [ + ZetaDropdownItem( + value: "Item 1", + icon: Icon(ZetaIcons.star_round), + ), + ZetaDropdownItem( + value: "Item 2", + icon: Icon(ZetaIcons.star_half_round), + ), + ZetaDropdownItem( + value: "Item 3", + ), + ]; return ExampleScaffold( name: 'Select Input', @@ -26,84 +35,47 @@ class _SelectInputExampleState extends State { child: SingleChildScrollView( child: SizedBox( width: 320, - child: Column( - children: [ - ZetaSelectInput( - label: Row( - children: [ - Text('Label'), - Padding( - padding: const EdgeInsets.only(left: 6), - child: Text( - '*', - style: TextStyle(color: zeta.colors.red.shade60), - ), - ), - ], + child: Form( + key: formKey, + child: Column( + children: [ + ZetaSelectInput( + label: 'Large', + size: ZetaWidgetSize.large, + hintText: 'Default hint text', + rounded: false, + placeholder: 'Placeholder', + initialValue: "Item 1", + items: items, + ), + ZetaSelectInput( + label: 'Medium', + hintText: 'Default hint text', + placeholder: 'Placeholder', + items: items, + ), + ZetaSelectInput( + label: 'Small', + size: ZetaWidgetSize.small, + hintText: 'Default hint text', + placeholder: 'Placeholder', + items: items, + ), + ZetaSelectInput( + label: 'Disabled', + hintText: 'Default hint text', + placeholder: 'Placeholder', + disabled: true, + items: items, ), - hint: 'Default hint text', - leadingIcon: Icon(ZetaIcons.star_round), - hasError: _errorText != null, - errorText: _errorText, - onChanged: (item) { - setState(() { - selectedItem = item; - if (item != null) { - _errorText = null; - } - }); - }, - onTextChanged: (value) { - setState(() { - if (value.isEmpty) { - _errorText = 'Required'; - } else { - _errorText = null; - } - }); - }, - selectedItem: selectedItem, - items: [ - ZetaSelectInputItem( - value: 'Item 1', - ), - ZetaSelectInputItem( - value: 'Item 2', - ), - ZetaSelectInputItem( - value: 'Item 3', - ), - ZetaSelectInputItem( - value: 'Item 4', - ), - ZetaSelectInputItem( - value: 'Item 5', - ), - ZetaSelectInputItem( - value: 'Item 6', - ), - ZetaSelectInputItem( - value: 'Item 7', - ), - ZetaSelectInputItem( - value: 'Item 8', - ), - ZetaSelectInputItem( - value: 'Item 9', - ), - ZetaSelectInputItem( - value: 'Item 10', - ), - ZetaSelectInputItem( - value: 'Item 11', - ), - ZetaSelectInputItem( - value: 'Item 12', - ), - ], - ), - const SizedBox(height: 120), - ], + ZetaButton( + label: 'Validate', + onPressed: () { + formKey.currentState?.validate(); + }, + ) + ].divide(const SizedBox(height: 8)).toList(), + ), ), ), ), diff --git a/example/lib/pages/components/text_input_example.dart b/example/lib/pages/components/text_input_example.dart index 0b293921..80bbb133 100644 --- a/example/lib/pages/components/text_input_example.dart +++ b/example/lib/pages/components/text_input_example.dart @@ -44,12 +44,16 @@ class TextInputExample extends StatelessWidget { ZetaTextInput( size: ZetaWidgetSize.small, placeholder: 'Placeholder', - prefix: IconButton( - iconSize: 12, - icon: Icon( - ZetaIcons.add_alert_round, + prefix: SizedBox( + height: 8, + child: IconButton( + iconSize: 12, + splashRadius: 1, + icon: Icon( + ZetaIcons.add_alert_round, + ), + onPressed: () {}, ), - onPressed: () {}, ), ), const SizedBox(height: 8), diff --git a/example/widgetbook/pages/components/select_input_widgetbook.dart b/example/widgetbook/pages/components/select_input_widgetbook.dart index 2a6d13c3..64c378fb 100644 --- a/example/widgetbook/pages/components/select_input_widgetbook.dart +++ b/example/widgetbook/pages/components/select_input_widgetbook.dart @@ -6,44 +6,44 @@ import '../../test/test_components.dart'; import '../../utils/utils.dart'; Widget selectInputUseCase(BuildContext context) { - final zeta = Zeta.of(context); final items = [ - ZetaSelectInputItem(value: 'Item 1'), - ZetaSelectInputItem(value: 'Item 2'), - ZetaSelectInputItem(value: 'Item 3'), - ZetaSelectInputItem(value: 'Item 4'), - ZetaSelectInputItem(value: 'Item 5'), - ZetaSelectInputItem(value: 'Item 6'), - ZetaSelectInputItem(value: 'Item 7'), - ZetaSelectInputItem(value: 'Item 8'), - ZetaSelectInputItem(value: 'Item 9'), - ZetaSelectInputItem(value: 'Item 10'), - ZetaSelectInputItem(value: 'Item 11'), - ZetaSelectInputItem(value: 'Item 12'), + ZetaDropdownItem( + value: "Item 1", + icon: Icon(ZetaIcons.star_round), + ), + ZetaDropdownItem( + value: "Item 2", + icon: Icon(ZetaIcons.star_half_round), + ), + ZetaDropdownItem( + value: "Item 3", + ), ]; - late ZetaSelectInputItem? selectedItem = items.first; - String? _errorText; final label = context.knobs.string( label: 'Label', initialValue: 'Label', ); - final hint = context.knobs.string( + final errorText = context.knobs.stringOrNull( + label: 'Error message', + initialValue: 'Oops! Error hint text', + ); + final hintText = context.knobs.string( label: 'Hint', initialValue: 'Default hint text', ); - final rounded = context.knobs.boolean(label: 'Rounded', initialValue: true); - final enabled = context.knobs.boolean(label: 'Enabled', initialValue: true); - final required = context.knobs.boolean(label: 'Required', initialValue: true); + final rounded = roundedKnob(context); + final disabled = disabledKnob(context); + final size = context.knobs.list( label: 'Size', options: ZetaWidgetSize.values, - labelBuilder: (size) => size.name, + labelBuilder: (size) => enumLabelBuilder(size), ); - final iconData = iconKnob( - context, - name: "Icon", - rounded: rounded, - initial: rounded ? ZetaIcons.star_round : ZetaIcons.star_sharp, + + final requirementLevel = context.knobs.list( + label: 'Requirement Level', + options: ZetaFormFieldRequirement.values, + labelBuilder: (requirementLevel) => enumLabelBuilder(requirementLevel), ); return WidgetbookTestWidget( @@ -53,44 +53,13 @@ Widget selectInputUseCase(BuildContext context) { padding: const EdgeInsets.all(ZetaSpacing.xL2), child: ZetaSelectInput( rounded: rounded, - enabled: enabled, + disabled: disabled, size: size, - label: Row( - children: [ - Text(label), - if (required) - Padding( - padding: const EdgeInsets.only(left: 6), - child: Text( - '*', - style: TextStyle(color: zeta.colors.red.shade60), - ), - ), - ], - ), - hint: hint, - leadingIcon: Icon(iconData), - hasError: _errorText != null, - errorText: _errorText, - onChanged: (item) { - setState(() { - selectedItem = item; - if (item != null) { - _errorText = null; - } - }); - }, - onTextChanged: (value) { - setState(() { - if (required && value.isEmpty) { - _errorText = 'Required'; - } else { - _errorText = null; - } - }); - }, - selectedItem: selectedItem, items: items, + label: label, + hintText: hintText, + requirementLevel: requirementLevel, + errorText: errorText, ), ); }, diff --git a/lib/src/components/buttons/input_icon_button.dart b/lib/src/components/buttons/input_icon_button.dart new file mode 100644 index 00000000..2377499f --- /dev/null +++ b/lib/src/components/buttons/input_icon_button.dart @@ -0,0 +1,73 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zeta_flutter.dart'; + +/// An icon button to be used internally in inputs +class InputIconButton extends StatelessWidget { + /// Creates a new [InputIconButton] + const InputIconButton({ + super.key, + required this.icon, + required this.onTap, + required this.disabled, + required this.size, + required this.color, + }); + + /// The icon + final IconData icon; + + /// On tap + final VoidCallback onTap; + + /// Disables the icon and its on tap + final bool disabled; + + /// The size of the icon + final ZetaWidgetSize size; + + /// The color of the icon + final Color color; + + double get _iconSize { + switch (size) { + case ZetaWidgetSize.large: + return ZetaSpacing.xL2; + case ZetaWidgetSize.medium: + return ZetaSpacing.xL; + case ZetaWidgetSize.small: + return ZetaSpacing.large; + } + } + + @override + Widget build(BuildContext context) { + final colors = Zeta.of(context).colors; + + return IconButton( + padding: EdgeInsets.all(_iconSize / 2), + constraints: BoxConstraints( + maxHeight: _iconSize * 2, + maxWidth: _iconSize * 2, + minHeight: _iconSize * 2, + minWidth: _iconSize * 2, + ), + color: !disabled ? color : colors.iconDisabled, + onPressed: disabled ? null : onTap, + iconSize: _iconSize, + icon: Icon(icon), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('icon', icon)) + ..add(ObjectFlagProperty.has('onTap', onTap)) + ..add(DiagnosticsProperty('disabled', disabled)) + ..add(ColorProperty('color', color)) + ..add(EnumProperty('size', size)); + } +} diff --git a/lib/src/components/date_input/date_input.dart b/lib/src/components/date_input/date_input.dart index de2c15cd..ccc47150 100644 --- a/lib/src/components/date_input/date_input.dart +++ b/lib/src/components/date_input/date_input.dart @@ -6,6 +6,7 @@ import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; import '../../../zeta_flutter.dart'; import '../../interfaces/form_field.dart'; +import '../buttons/input_icon_button.dart'; /// A form field used to input dates. /// @@ -107,17 +108,6 @@ class ZetaDateInputState extends State implements ZetaFormFieldSt bool get _showClearButton => _controller.text.isNotEmpty; - double get _iconSize { - switch (widget.size) { - case ZetaWidgetSize.large: - return ZetaSpacing.xL2; - case ZetaWidgetSize.medium: - return ZetaSpacing.xL; - case ZetaWidgetSize.small: - return ZetaSpacing.large; - } - } - DateTime? get _value { final value = _dateFormatter.getMaskedText().trim(); final date = DateFormat(widget.dateFormat).tryParseStrict(value); @@ -259,18 +249,18 @@ class ZetaDateInputState extends State implements ZetaFormFieldSt mainAxisSize: MainAxisSize.min, children: [ if (_showClearButton) - _IconButton( + InputIconButton( icon: widget.rounded ? ZetaIcons.cancel_round : ZetaIcons.cancel_sharp, onTap: reset, disabled: widget.disabled, - size: _iconSize, + size: widget.size, color: _colors.iconSubtle, ), - _IconButton( + InputIconButton( icon: widget.rounded ? ZetaIcons.calendar_round : ZetaIcons.calendar_sharp, onTap: _pickDate, disabled: widget.disabled, - size: _iconSize, + size: widget.size, color: _colors.iconDefault, ), ], @@ -278,49 +268,3 @@ class ZetaDateInputState extends State implements ZetaFormFieldSt ); } } - -class _IconButton extends StatelessWidget { - const _IconButton({ - required this.icon, - required this.onTap, - required this.disabled, - required this.size, - required this.color, - }); - - final IconData icon; - final VoidCallback onTap; - final bool disabled; - final double size; - final Color color; - - @override - Widget build(BuildContext context) { - final colors = Zeta.of(context).colors; - - return IconButton( - padding: EdgeInsets.all(size / 2), - constraints: BoxConstraints( - maxHeight: size * 2, - maxWidth: size * 2, - minHeight: size * 2, - minWidth: size * 2, - ), - color: !disabled ? color : colors.iconDisabled, - onPressed: disabled ? null : onTap, - iconSize: size, - icon: Icon(icon), - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('icon', icon)) - ..add(ObjectFlagProperty.has('onTap', onTap)) - ..add(DiagnosticsProperty('disabled', disabled)) - ..add(DoubleProperty('size', size)) - ..add(ColorProperty('color', color)); - } -} diff --git a/lib/src/components/dropdown/dropdown.dart b/lib/src/components/dropdown/dropdown.dart index ba279d51..017f304c 100644 --- a/lib/src/components/dropdown/dropdown.dart +++ b/lib/src/components/dropdown/dropdown.dart @@ -26,7 +26,42 @@ enum ZetaDropdownSize { mini, } -/// An item used in a [ZetaDropdown]. +/// A class for controlling a [ZetaDropdown] +/// +/// Can be accquired from the builder method of a [ZetaDropdown] +abstract class ZetaDropdownController { + /// Returns true if the dropdown is open. + bool get isOpen; + + /// Opens the dropdown. + void open(); + + /// Closes the dropdown. + void close(); + + /// Toggles the dropdown open or closed depending on its current state. + void toggle(); +} + +class _DropdownControllerImpl implements ZetaDropdownController { + _DropdownControllerImpl({required this.overlayPortalController}); + + final OverlayPortalController overlayPortalController; + + @override + bool get isOpen => overlayPortalController.isShowing; + + @override + void close() => overlayPortalController.hide(); + + @override + void open() => overlayPortalController.show(); + + @override + void toggle() => overlayPortalController.toggle(); +} + +/// An item used in a [ZetaDropdown] or a [ZetaSelectInput]. class ZetaDropdownItem { /// Creates a new [ZetaDropdownItem] ZetaDropdownItem({ @@ -60,8 +95,10 @@ class ZetaDropdown extends StatefulWidget { this.rounded = true, this.type = ZetaDropdownMenuType.standard, this.size = ZetaDropdownSize.standard, + this.builder, + this.onDismissed, super.key, - }) : assert(items.length > 0, 'Items must be greater than 0.'); + }); /// The items displayed in the dropdown. final List> items; @@ -74,7 +111,10 @@ class ZetaDropdown extends StatefulWidget { /// Called with the selected value whenever the dropdown is changed. /// /// {@macro on-change-disable} - final ValueSetter? onChange; + final ValueSetter>? onChange; + + /// Called when the dropdown is dimissed. + final VoidCallback? onDismissed; /// {@macro zeta-component-rounded} final bool rounded; @@ -89,8 +129,17 @@ class ZetaDropdown extends StatefulWidget { /// Defaults to [ZetaDropdownSize.mini] final ZetaDropdownSize size; + /// A custom builder for the child of the dropdown. + /// + /// Provides a build context, the currently selected item in the dropdown and a controller which can be used to open/close the dropdown. + final Widget Function( + BuildContext context, + ZetaDropdownItem? selectedItem, + ZetaDropdownController controller, + )? builder; + @override - State> createState() => _ZetaDropDownState(); + State> createState() => ZetaDropDownState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -99,8 +148,17 @@ class ZetaDropdown extends StatefulWidget { ..add(DiagnosticsProperty('rounded', rounded)) ..add(IterableProperty>('items', items)) ..add(DiagnosticsProperty('selectedItem', value)) - ..add(ObjectFlagProperty?>.has('onChange', onChange)) - ..add(EnumProperty('size', size)); + ..add(EnumProperty('size', size)) + ..add(ObjectFlagProperty>?>.has('onChange', onChange)) + ..add(ObjectFlagProperty.has('onDismissed', onDismissed)) + ..add( + ObjectFlagProperty< + Widget Function( + BuildContext context, + ZetaDropdownItem? selectedItem, + ZetaDropdownController controller, + )?>.has('builder', builder), + ); } } @@ -113,11 +171,14 @@ enum MenuPosition { down, } -class _ZetaDropDownState extends State> { - final OverlayPortalController _tooltipController = OverlayPortalController(); +/// The state for a [ZetaDropdown] +class ZetaDropDownState extends State> { + final _DropdownControllerImpl _dropdownController = _DropdownControllerImpl( + overlayPortalController: OverlayPortalController(), + ); final _link = LayerLink(); final _menuKey = GlobalKey(); - final _headerKey = GlobalKey(); + final _childKey = GlobalKey(); MenuPosition _menuPosition = MenuPosition.down; ZetaDropdownItem? _selectedItem; @@ -132,16 +193,19 @@ class _ZetaDropDownState extends State> { _setSelectedItem(); } + /// Returns true if the dropdown is open. + bool get isOpen => _dropdownController.isOpen; + @override void didUpdateWidget(ZetaDropdown oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.value != widget.value) { setState(_setSelectedItem); } - if (widget.onChange != null) { + if (widget.onChange == null) { unawaited( Future.delayed(Duration.zero).then( - (value) => _tooltipController.hide(), + (value) => _dropdownController.close(), ), ); } @@ -149,15 +213,17 @@ class _ZetaDropDownState extends State> { /// Return position of header Offset get _headerPos { - final headerBox = _headerKey.currentContext!.findRenderObject()! as RenderBox; + final headerBox = _childKey.currentContext!.findRenderObject()! as RenderBox; return headerBox.localToGlobal(Offset.zero); } void _setSelectedItem() { - try { - _selectedItem = widget.items.firstWhere((item) => item.value == widget.value); - } catch (e) { - _selectedItem = widget.items.first; + if (widget.items.isNotEmpty) { + try { + _selectedItem = widget.items.firstWhere((item) => item.value == widget.value); + } catch (e) { + _selectedItem = null; + } } } @@ -175,12 +241,29 @@ class _ZetaDropDownState extends State> { @override Widget build(BuildContext context) { + late Widget child; + + if (widget.builder != null) { + child = Container( + key: _childKey, + child: widget.builder!(context, _selectedItem, _dropdownController), + ); + } else { + child = _DropdownItem( + onPress: widget.onChange != null ? _onTap : null, + value: _selectedItem ?? widget.items.first, + allocateLeadingSpace: widget.type == ZetaDropdownMenuType.standard && _selectedItem?.icon != null, + rounded: widget.rounded, + key: _childKey, + ); + } + return SizedBox( width: _size, child: CompositedTransformTarget( link: _link, child: OverlayPortal( - controller: _tooltipController, + controller: _dropdownController.overlayPortalController, overlayChildBuilder: (BuildContext context) { return CompositedTransformFollower( link: _link, @@ -188,19 +271,24 @@ class _ZetaDropDownState extends State> { ? Alignment.topLeft : Alignment.bottomLeft, // Align overlay dropdown in its correct position followerAnchor: _menuPosition == MenuPosition.up ? Alignment.bottomLeft : Alignment.topLeft, + offset: const Offset(0, ZetaSpacing.xL * -1), child: Align( alignment: _menuPosition == MenuPosition.up ? AlignmentDirectional.bottomStart : AlignmentDirectional.topStart, child: TapRegion( onTapOutside: (event) { - final headerBox = _headerKey.currentContext!.findRenderObject()! as RenderBox; + final headerBox = _childKey.currentContext!.findRenderObject()! as RenderBox; + final headerPosition = headerBox.localToGlobal(Offset.zero); final inHeader = _isInHeader( headerPosition, headerBox.size, event.position, ); - if (!inHeader) _tooltipController.hide(); + if (!inHeader) { + _dropdownController.close(); + widget.onDismissed?.call(); + } }, child: _ZetaDropDownMenu( items: widget.items, @@ -214,21 +302,15 @@ class _ZetaDropDownState extends State> { setState(() { _selectedItem = item; }); - widget.onChange?.call(item.value); - _tooltipController.hide(); + widget.onChange?.call(item); + _dropdownController.close(); }, ), ), ), ); }, - child: _DropdownItem( - onPress: widget.onChange != null ? onTap : null, - value: _selectedItem ?? widget.items.first, - allocateLeadingSpace: widget.type == ZetaDropdownMenuType.standard && _selectedItem?.icon != null, - rounded: widget.rounded, - key: _headerKey, - ), + child: child, ), ), ); @@ -236,10 +318,10 @@ class _ZetaDropDownState extends State> { double get _size => widget.size == ZetaDropdownSize.mini ? 120 : 320; - void onTap() { + void _onTap() { /// Version 1 : Calculate if overflow happens based on using calculations from sizes. final height = MediaQuery.of(context).size.height; - final headerRenderBox = _headerKey.currentContext!.findRenderObject()! as RenderBox; + final headerRenderBox = _childKey.currentContext!.findRenderObject()! as RenderBox; final dropdownItemHeight = headerRenderBox.size.height; /// Calculate if overflow can happen @@ -255,18 +337,20 @@ class _ZetaDropDownState extends State> { }); } - _tooltipController.toggle(); + _dropdownController.toggle(); } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add( - DiagnosticsProperty>>( - 'menuKey', - _menuKey, - ), - ); + properties + ..add( + DiagnosticsProperty>>( + 'menuKey', + _menuKey, + ), + ) + ..add(DiagnosticsProperty('isOpen', isOpen)); } } @@ -338,6 +422,12 @@ class _DropdownItemState extends State<_DropdownItem> { }); } + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final colors = Zeta.of(context).colors; @@ -358,6 +448,7 @@ class _DropdownItemState extends State<_DropdownItem> { child: OutlinedButton( key: widget.itemKey, onPressed: widget.onPress, + statesController: controller, style: _getStyle(colors), child: Row( mainAxisSize: MainAxisSize.min, @@ -438,7 +529,7 @@ class _DropdownItemState extends State<_DropdownItem> { ), side: WidgetStateBorderSide.resolveWith((states) { if (states.contains(WidgetState.focused)) { - return BorderSide(color: colors.borderPrimary); + return BorderSide(color: colors.borderPrimary, width: ZetaSpacing.xL); } return BorderSide.none; }), diff --git a/lib/src/components/select_input/select_input.dart b/lib/src/components/select_input/select_input.dart index bbc3a07e..e7741beb 100644 --- a/lib/src/components/select_input/select_input.dart +++ b/lib/src/components/select_input/select_input.dart @@ -1,650 +1,194 @@ import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import '../../../zeta_flutter.dart'; - -enum _MenuPosition { top, bottom } +import '../../interfaces/form_field.dart'; +import '../buttons/input_icon_button.dart'; /// Class for [ZetaSelectInput] -class ZetaSelectInput extends StatefulWidget { +class ZetaSelectInput extends ZetaFormField { ///Constructor of [ZetaSelectInput] const ZetaSelectInput({ super.key, required this.items, - this.onChanged, this.onTextChanged, - this.selectedItem, - this.size, - this.leadingIcon, + this.size = ZetaWidgetSize.medium, this.label, - this.hint, - this.enabled = true, + this.hintText, this.rounded = true, - this.hasError = false, + this.prefix, + this.placeholder, + this.validator, this.errorText, + super.disabled = false, + super.initialValue, + super.onChange, + super.requirementLevel = ZetaFormFieldRequirement.none, }); - /// Input items as list of [ZetaSelectInputItem] - final List items; - - /// Currently selected item - final ZetaSelectInputItem? selectedItem; - - /// Handles changes of select menu - final ValueSetter? onChanged; + /// Input items as list of [ZetaDropdownItem] + final List> items; /// Handles changes of input text final ValueSetter? onTextChanged; - /// Determines the size of the input field. - /// Default is `ZetaDateInputSize.large` - final ZetaWidgetSize? size; + /// The prefix widget for the input. + /// + /// Will be overriden if the selected item has an icon. + final Widget? prefix; - /// The input's leading icon. - final Widget? leadingIcon; + /// The error text shown beneath the input when the validator fails. + final String? errorText; - /// If provided, displays a label above the input field. - final Widget? label; + /// The validator for the input. + final String? Function(T? value)? validator; - /// If provided, displays a hint below the input field. - final String? hint; + /// Determines the size of the input field. + /// Defaults to [ZetaWidgetSize.medium] + final ZetaWidgetSize size; - /// Determines if the input field should be enabled (default) or disabled. - final bool enabled; + /// If provided, displays a label above the input field. + final String? label; - /// Determines if the input field should be displayed in error style. - /// Default is `false`. - /// If `enabled` is `false`, this has no effect. - final bool hasError; + /// If provided, displays a hint below the input field. + final String? hintText; - /// In combination with `hasError: true`, provides the error message - /// to be displayed below the input field. - final String? errorText; + /// The placeholder for the input. + final String? placeholder; /// {@macro zeta-component-rounded} final bool rounded; @override - State createState() => _ZetaSelectInputState(); + State> createState() => _ZetaSelectInputState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('rounded', rounded)) - ..add( - ObjectFlagProperty?>.has( - 'onChanged', - onChanged, - ), - ) ..add(EnumProperty('size', size)) - ..add(StringProperty('hint', hint)) - ..add(DiagnosticsProperty('enabled', enabled)) - ..add(DiagnosticsProperty('hasError', hasError)) + ..add(StringProperty('hint', hintText)) + ..add(ObjectFlagProperty?>.has('onTextChanged', onTextChanged)) + ..add(IterableProperty>('items', items)) ..add(StringProperty('errorText', errorText)) - ..add(ObjectFlagProperty?>.has('onTextChanged', onTextChanged)); + ..add(ObjectFlagProperty.has('validator', validator)) + ..add(StringProperty('label', label)) + ..add(StringProperty('placeholder', placeholder)); } } -class _ZetaSelectInputState extends State { - final OverlayPortalController _overlayController = OverlayPortalController(); - final _link = LayerLink(); - late String? _selectedValue; - late List _menuItems; - Size _menuSize = Size.zero; - _MenuPosition? _menuPosition = _MenuPosition.bottom; +class _ZetaSelectInputState extends State> { + final GlobalKey> _dropdownKey = GlobalKey(); + final TextEditingController _inputController = TextEditingController(); - @override - void initState() { - super.initState(); - _selectedValue = widget.selectedItem?.value; - _menuItems = List.from(widget.items); - } + ZetaDropdownItem? _selectedItem; - @override - Widget build(BuildContext context) { - return CompositedTransformTarget( - link: _link, - child: OverlayPortal( - controller: _overlayController, - overlayChildBuilder: (BuildContext context) { - return CompositedTransformFollower( - link: _link, - targetAnchor: _menuPosition == _MenuPosition.top ? Alignment.topLeft : Alignment.bottomLeft, - followerAnchor: _menuPosition == _MenuPosition.top ? Alignment.bottomLeft : Alignment.topLeft, - child: Align( - alignment: _menuPosition == _MenuPosition.top ? Alignment.bottomLeft : Alignment.topLeft, - child: _ZetaSelectInputMenu( - size: _menuSize, - itemSize: widget.size, - items: _menuItems, - selectedValue: _selectedValue, - onSelected: (item) { - if (item != null) { - _selectedValue = item.value; - widget.onChanged?.call(item); - } - _overlayController.hide(); - }, - rounded: widget.rounded, - ), - ), - ); - }, - child: _InputComponent( - size: widget.size, - label: widget.label, - hint: widget.hint, - leadingIcon: widget.leadingIcon, - enabled: widget.enabled, - rounded: widget.rounded, - hasError: widget.hasError, - errorText: widget.errorText, - initialValue: _selectedValue, - onToggleMenu: widget.items.isEmpty - ? null - : () { - if (_overlayController.isShowing) { - _overlayController.hide(); - return setState(() {}); - } - final box = context.findRenderObject() as RenderBox?; - final offset = box?.size.topLeft( - box.localToGlobal(Offset.zero), - ); - final upperHeight = offset?.dy ?? 0; - final lowerHeight = MediaQuery.of(context).size.height - upperHeight - (box?.size.height ?? 0); - setState(() { - _menuPosition = upperHeight > lowerHeight ? _MenuPosition.top : _MenuPosition.bottom; - _menuSize = Size( - box?.size.width ?? (MediaQuery.of(context).size.width - ZetaSpacing.xL6), - (upperHeight > lowerHeight ? upperHeight : lowerHeight) - ZetaSpacing.xL2, - ); - _menuItems = List.from(widget.items); - }); - _overlayController.show(); - }, - menuIsShowing: _overlayController.isShowing, - onChanged: (value) { - widget.onTextChanged?.call(value); - _selectedValue = value; - _menuItems = widget.items - .where( - (item) => item.value.toLowerCase().contains(value.toLowerCase()), - ) - .toList(); - final item = widget.items.firstWhereOrNull( - (item) => item.value.toLowerCase() == value.toLowerCase(), - ); - widget.onChanged?.call(item); - setState(() {}); - }, - ), - ), - ); - } -} + bool get _dropdownOpen => _dropdownKey.currentState?.isOpen ?? false; -class _InputComponent extends StatefulWidget { - const _InputComponent({ - this.size, - this.label, - this.hint, - this.leadingIcon, - this.enabled = true, - this.rounded = true, - this.hasError = false, - this.errorText, - this.initialValue, - this.onChanged, - this.onToggleMenu, - this.menuIsShowing = false, - }); - - final ZetaWidgetSize? size; - final Widget? label; - final String? hint; - final Widget? leadingIcon; - final bool enabled; - final bool rounded; - final bool hasError; - final String? errorText; - final String? initialValue; - final void Function(String)? onChanged; - final VoidCallback? onToggleMenu; - final bool menuIsShowing; - - @override - State<_InputComponent> createState() => _InputComponentState(); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(EnumProperty('size', size)) - ..add(StringProperty('hint', hint)) - ..add(DiagnosticsProperty('enabled', enabled)) - ..add(DiagnosticsProperty('rounded', rounded)) - ..add(DiagnosticsProperty('hasError', hasError)) - ..add(StringProperty('errorText', errorText)) - ..add(ObjectFlagProperty.has('onChanged', onChanged)) - ..add(ObjectFlagProperty.has('onToggleMenu', onToggleMenu)) - ..add(DiagnosticsProperty('menuIsShowing', menuIsShowing)) - ..add(StringProperty('initialValue', initialValue)); + IconData get _icon { + if (_dropdownOpen) { + return widget.rounded ? ZetaIcons.expand_less_round : ZetaIcons.expand_less_sharp; + } else { + return widget.rounded ? ZetaIcons.expand_more_round : ZetaIcons.expand_more_sharp; + } } -} - -class _InputComponentState extends State<_InputComponent> { - final _controller = TextEditingController(); - late ZetaWidgetSize _size; - bool _hasError = false; @override void initState() { + _inputController.addListener( + () => setState(() {}), + ); + _setInitialItem(); super.initState(); - _setParams(); } @override - void didUpdateWidget(_InputComponent oldWidget) { + void didUpdateWidget(covariant ZetaSelectInput oldWidget) { + if (oldWidget.initialValue != widget.initialValue) { + setState(_setInitialItem); + } super.didUpdateWidget(oldWidget); - _setParams(); - } - - void _setParams() { - _controller.text = widget.initialValue ?? ''; - _size = widget.size ?? ZetaWidgetSize.large; - _hasError = widget.hasError; } - @override - void dispose() { - _controller.dispose(); - super.dispose(); + void _setInitialItem() { + _selectedItem = widget.items.firstWhereOrNull((item) => item.value == widget.initialValue); + _inputController.text = _selectedItem?.label ?? ''; } - @override - Widget build(BuildContext context) { - final zeta = Zeta.of(context); - final showError = _hasError && widget.errorText != null; - final hintErrorColor = widget.enabled - ? showError - ? zeta.colors.red - : zeta.colors.cool.shade70 - : zeta.colors.cool.shade50; - final iconSize = _iconSize(_size); - final inputVerticalPadding = _inputVerticalPadding(_size); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.label != null) - Padding( - padding: const EdgeInsets.only(bottom: 5), - child: DefaultTextStyle( - style: ZetaTextStyles.bodyMedium.copyWith( - color: widget.enabled ? zeta.colors.textDefault : zeta.colors.cool.shade50, - ), - child: widget.label!, - ), - ), - TextFormField( - enabled: widget.enabled, - controller: _controller, - onChanged: widget.onChanged, - style: _size == ZetaWidgetSize.small ? ZetaTextStyles.bodyXSmall : ZetaTextStyles.bodyMedium, - decoration: InputDecoration( - isDense: true, - contentPadding: EdgeInsets.symmetric( - horizontal: 10, - vertical: inputVerticalPadding, - ), - prefixIcon: widget.leadingIcon == null - ? null - : Padding( - padding: const EdgeInsets.only(left: ZetaSpacingBase.x2_5, right: ZetaSpacing.small), - child: IconTheme( - data: IconThemeData( - color: widget.enabled ? zeta.colors.cool.shade70 : zeta.colors.cool.shade50, - size: iconSize, - ), - child: widget.leadingIcon!, - ), - ), - prefixIconConstraints: const BoxConstraints( - minHeight: ZetaSpacing.xL2, - minWidth: ZetaSpacing.xL2, - ), - suffixIcon: widget.onToggleMenu == null - ? null - : Padding( - padding: const EdgeInsets.only(right: ZetaSpacing.minimum), - child: IconButton( - visualDensity: const VisualDensity( - horizontal: -4, - vertical: -4, - ), - onPressed: widget.onToggleMenu, - icon: Icon( - widget.menuIsShowing - ? (widget.rounded ? ZetaIcons.expand_less_round : ZetaIcons.expand_less_sharp) - : (widget.rounded ? ZetaIcons.expand_more_round : ZetaIcons.expand_more_sharp), - color: widget.enabled ? zeta.colors.textDefault : zeta.colors.cool.shade50, - size: iconSize, - ), - ), - ), - suffixIconConstraints: const BoxConstraints( - minHeight: ZetaSpacing.xL2, - minWidth: ZetaSpacing.xL2, - ), - hintStyle: _size == ZetaWidgetSize.small - ? ZetaTextStyles.bodyXSmall.copyWith( - color: widget.enabled ? zeta.colors.textDefault : zeta.colors.cool.shade50, - ) - : ZetaTextStyles.bodyMedium.copyWith( - color: widget.enabled ? zeta.colors.textDefault : zeta.colors.cool.shade50, - ), - filled: !widget.enabled || _hasError ? true : null, - fillColor: widget.enabled - ? _hasError - ? zeta.colors.red.shade10 - : null - : zeta.colors.cool.shade30, - enabledBorder: _hasError - ? _errorInputBorder(zeta, rounded: widget.rounded) - : _defaultInputBorder(zeta, rounded: widget.rounded), - focusedBorder: _hasError - ? _errorInputBorder(zeta, rounded: widget.rounded) - : _focusedInputBorder(zeta, rounded: widget.rounded), - disabledBorder: _defaultInputBorder(zeta, rounded: widget.rounded), - errorBorder: _errorInputBorder(zeta, rounded: widget.rounded), - focusedErrorBorder: _errorInputBorder(zeta, rounded: widget.rounded), - ), - ), - if (widget.hint != null || showError) - Padding( - padding: const EdgeInsets.only(top: 5), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: ZetaSpacing.small), - child: Icon( - showError && widget.enabled - ? (widget.rounded ? ZetaIcons.error_round : ZetaIcons.error_sharp) - : (widget.rounded ? ZetaIcons.info_round : ZetaIcons.info_sharp), - size: ZetaSpacing.large, - color: hintErrorColor, - ), - ), - Expanded( - child: Text( - showError && widget.enabled ? widget.errorText! : widget.hint!, - style: ZetaTextStyles.bodyXSmall.copyWith( - color: hintErrorColor, - ), - ), - ), - ], - ), - ), - ], - ); + void _onInputChanged(ZetaDropdownController dropdownController) { + dropdownController.open(); + setState(() { + _selectedItem = null; + }); + widget.onChange?.call(null); } - double _inputVerticalPadding(ZetaWidgetSize size) => switch (size) { - ZetaWidgetSize.large => ZetaSpacing.medium, - ZetaWidgetSize.medium => ZetaSpacing.small, - ZetaWidgetSize.small => ZetaSpacing.small, - }; - - double _iconSize(ZetaWidgetSize size) => switch (size) { - ZetaWidgetSize.large => ZetaSpacing.xL, - ZetaWidgetSize.medium => ZetaSpacing.xL, - ZetaWidgetSize.small => ZetaSpacing.large, - }; - - OutlineInputBorder _defaultInputBorder( - Zeta zeta, { - required bool rounded, - }) => - OutlineInputBorder( - borderRadius: rounded ? ZetaRadius.minimal : ZetaRadius.none, - borderSide: BorderSide(color: zeta.colors.cool.shade40), - ); - - OutlineInputBorder _focusedInputBorder( - Zeta zeta, { - required bool rounded, - }) => - OutlineInputBorder( - borderRadius: rounded ? ZetaRadius.minimal : ZetaRadius.none, - borderSide: BorderSide(color: zeta.colors.blue.shade50), - ); - - OutlineInputBorder _errorInputBorder( - Zeta zeta, { - required bool rounded, - }) => - OutlineInputBorder( - borderRadius: rounded ? ZetaRadius.minimal : ZetaRadius.none, - borderSide: BorderSide(color: zeta.colors.red.shade50), - ); -} - -/// Class for [ZetaSelectInputItem] -class ZetaSelectInputItem extends StatelessWidget { - ///Public constructor for [ZetaSelectInputItem] - const ZetaSelectInputItem({ - super.key, - required this.value, - this.size = ZetaWidgetSize.large, - }) : rounded = true, - selected = false, - onPressed = null; - - const ZetaSelectInputItem._({ - super.key, - required this.rounded, - required this.selected, - required this.value, - this.onPressed, - this.size = ZetaWidgetSize.large, - }); - - /// {@macro zeta-component-rounded} - final bool rounded; - - /// If [ZetaSelectInputItem] is selected - final bool selected; - - /// Value of [ZetaSelectInputItem] - final String value; - - /// Handles clicking for [ZetaSelectInputItem] - final VoidCallback? onPressed; - - /// The size of [ZetaSelectInputItem] - final ZetaWidgetSize size; - - /// Returns copy of [ZetaSelectInputItem] with those private variables included - ZetaSelectInputItem copyWith({ - bool? rounded, - bool? selected, - VoidCallback? onPressed, - ZetaWidgetSize? size, - }) { - return ZetaSelectInputItem._( - rounded: rounded ?? this.rounded, - selected: selected ?? this.selected, - onPressed: onPressed ?? this.onPressed, - size: size ?? this.size, - value: value, - key: key, - ); + void _onIconTapped(ZetaDropdownController dropdownController) { + dropdownController.toggle(); + setState(() {}); } - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('rounded', rounded)) - ..add(DiagnosticsProperty('selected', selected)) - ..add(StringProperty('value', value)) - ..add(ObjectFlagProperty.has('onPressed', onPressed)) - ..add(EnumProperty('size', size)); + void _onDropdownChanged(ZetaDropdownItem item) { + _inputController.text = item.label; + setState(() { + _selectedItem = item; + }); + widget.onChange?.call(item.value); } @override Widget build(BuildContext context) { final colors = Zeta.of(context).colors; - return DefaultTextStyle( - style: ZetaTextStyles.bodyMedium, - child: OutlinedButton( - onPressed: onPressed, - style: _getStyle(colors, size), - child: Text(value), - ), - ); - } - - ButtonStyle _getStyle(ZetaColors colors, ZetaWidgetSize size) { - final visualDensity = switch (size) { - ZetaWidgetSize.large => 0.0, - ZetaWidgetSize.medium => -2.0, - ZetaWidgetSize.small => -4.0, - }; - return ButtonStyle( - backgroundColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.hovered)) { - return colors.surfaceHover; - } - - if (states.contains(WidgetState.pressed)) { - return colors.surfaceSelected; - } - - if (states.contains(WidgetState.disabled) || onPressed == null) { - return colors.surfaceDisabled; - } - return colors.surfacePrimary; - }), - foregroundColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.disabled)) { - return colors.textDisabled; - } - return colors.textDefault; - }), - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: rounded ? ZetaRadius.minimal : ZetaRadius.none, - ), - ), - side: WidgetStatePropertyAll( - selected ? BorderSide(color: colors.primary.shade60) : BorderSide.none, - ), - padding: const WidgetStatePropertyAll( - EdgeInsets.symmetric(horizontal: ZetaSpacing.large), - ), - elevation: const WidgetStatePropertyAll(0), - overlayColor: const WidgetStatePropertyAll(Colors.transparent), - textStyle: WidgetStatePropertyAll( - size == ZetaWidgetSize.small ? ZetaTextStyles.bodyXSmall : ZetaTextStyles.bodyMedium, - ), - minimumSize: const WidgetStatePropertyAll(Size.fromHeight(ZetaSpacing.xL8)), - alignment: Alignment.centerLeft, - visualDensity: VisualDensity( - horizontal: visualDensity, - vertical: visualDensity, - ), - ); - } -} - -class _ZetaSelectInputMenu extends StatelessWidget { - const _ZetaSelectInputMenu({ - required this.items, - required this.onSelected, - required this.size, - this.selectedValue, - this.rounded = true, - this.itemSize, - }); - - /// Input items for the menu - final List items; - - /// Handles selecting an item from the menu - final ValueSetter onSelected; - - /// The value of the currently selected item - final String? selectedValue; - - /// The size of the menu. - final Size size; - - /// {@macro zeta-component-rounded} - final bool rounded; - - /// The size of [ZetaSelectInputItem] - final ZetaWidgetSize? itemSize; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add( - ObjectFlagProperty>.has( - 'onSelected', - onSelected, - ), - ) - ..add(DiagnosticsProperty('rounded', rounded)) - ..add(StringProperty('selectedValue', selectedValue)) - ..add(DiagnosticsProperty('size', size)) - ..add(EnumProperty('itemSize', itemSize)); - } - - @override - Widget build(BuildContext context) { - final colors = Zeta.of(context).colors; - return ConstrainedBox( - constraints: BoxConstraints( - maxWidth: size.width, - maxHeight: size.height, - ), - child: DecoratedBox( - decoration: BoxDecoration( - color: colors.surfacePrimary, - borderRadius: rounded ? ZetaRadius.minimal : ZetaRadius.none, - boxShadow: const [ - BoxShadow(blurRadius: 2, color: Color.fromRGBO(40, 51, 61, 0.04)), - BoxShadow( - blurRadius: 8, - color: Color.fromRGBO(96, 104, 112, 0.16), - blurStyle: BlurStyle.outer, - offset: Offset(0, 4), - ), - ], - ), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: items.map((item) { - return item.copyWith( - rounded: rounded, - selected: selectedValue?.toLowerCase() == item.value.toLowerCase(), - onPressed: () => onSelected(item), - size: itemSize, - ); - }).toList(), + late List> filteredItems; + if (_inputController.text.isNotEmpty) { + filteredItems = widget.items.where( + (item) { + return item.label.toLowerCase().startsWith(_inputController.text.toLowerCase()); + }, + ).toList(); + } else { + filteredItems = widget.items; + } + + return ZetaDropdown( + items: filteredItems, + onChange: !widget.disabled ? _onDropdownChanged : null, + key: _dropdownKey, + value: _selectedItem?.value, + onDismissed: () => setState(() {}), + builder: (context, _, controller) { + return ZetaTextInput( + size: widget.size, + requirementLevel: widget.requirementLevel, + disabled: widget.disabled, + validator: (_) { + final currentValue = _selectedItem?.value; + String? errorText; + final customValidation = widget.validator?.call(currentValue); + if ((currentValue == null && widget.requirementLevel != ZetaFormFieldRequirement.optional) || + customValidation != null) { + errorText = customValidation ?? widget.errorText ?? ''; + } + + return errorText; + }, + controller: _inputController, + prefix: _selectedItem?.icon ?? widget.prefix, + label: widget.label, + placeholder: widget.placeholder, + hintText: widget.hintText, + onChange: (val) => _onInputChanged(controller), + suffix: InputIconButton( + icon: _icon, + disabled: widget.disabled, + size: widget.size, + color: colors.iconSubtle, + onTap: () => _onIconTapped(controller), ), - ), - ), + ); + }, ); } } diff --git a/lib/src/components/text_input/text_input.dart b/lib/src/components/text_input/text_input.dart index 7216c40f..bf804a79 100644 --- a/lib/src/components/text_input/text_input.dart +++ b/lib/src/components/text_input/text_input.dart @@ -35,6 +35,7 @@ class ZetaTextInput extends ZetaFormField { this.prefixTextStyle, this.suffixText, this.suffixTextStyle, + this.onSubmit, }) : assert(initialValue == null || controller == null, 'Only one of initial value and controller can be accepted.'), assert(prefix == null || prefixText == null, 'Only one of prefix or prefixText can be accepted.'), assert(suffix == null || suffixText == null, 'Only one of suffix or suffixText can be accepted.'); @@ -42,6 +43,9 @@ class ZetaTextInput extends ZetaFormField { /// The label displayed above the input. final String? label; + /// Called when the input is submitted. + final void Function(String? val)? onSubmit; + /// The hint text displayed below the input. final String? hintText; @@ -107,7 +111,8 @@ class ZetaTextInput extends ZetaFormField { ..add(DiagnosticsProperty('rounded', rounded)) ..add(DiagnosticsProperty('disabled', disabled)) ..add(IterableProperty('inputFormatters', inputFormatters)) - ..add(EnumProperty('requirementLevel', requirementLevel)); + ..add(EnumProperty('requirementLevel', requirementLevel)) + ..add(ObjectFlagProperty.has('onSubmit', onSubmit)); } } @@ -117,6 +122,7 @@ class ZetaTextInputState extends State implements ZetaFormFieldSt final GlobalKey> _key = GlobalKey(); ZetaColors get _colors => Zeta.of(context).colors; + // TODO(mikecoomber): refactor to use WidgetStateController bool _hovered = false; String? _errorText; @@ -235,6 +241,9 @@ class ZetaTextInputState extends State implements ZetaFormFieldSt _errorText = widget.errorText; }); } + if (oldWidget.initialValue != widget.initialValue && widget.initialValue != null) { + _controller.text = widget.initialValue!; + } super.didUpdateWidget(oldWidget); } @@ -283,6 +292,7 @@ class ZetaTextInputState extends State implements ZetaFormFieldSt }); return _errorText; }, + onFieldSubmitted: widget.onSubmit, textAlignVertical: TextAlignVertical.center, onChanged: widget.onChange, style: _baseTextStyle, diff --git a/lib/src/components/time_input/time_input.dart b/lib/src/components/time_input/time_input.dart index 65be2fd3..2dd2af5d 100644 --- a/lib/src/components/time_input/time_input.dart +++ b/lib/src/components/time_input/time_input.dart @@ -5,6 +5,7 @@ import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; import '../../../zeta_flutter.dart'; import '../../interfaces/form_field.dart'; +import '../buttons/input_icon_button.dart'; const _maxHrValue = 23; const _max12HrValue = 12; @@ -104,17 +105,6 @@ class ZetaTimeInputState extends State implements ZetaFormFieldSt BorderRadius get _borderRadius => widget.rounded ? ZetaRadius.minimal : ZetaRadius.none; - double get _iconSize { - switch (widget.size) { - case ZetaWidgetSize.large: - return ZetaSpacing.xL2; - case ZetaWidgetSize.medium: - return ZetaSpacing.xL; - case ZetaWidgetSize.small: - return ZetaSpacing.large; - } - } - int get _hrsLimit => _use12Hr ? _max12HrValue : _maxHrValue; final int _minsLimit = _maxMinsValue; @@ -261,18 +251,18 @@ class ZetaTimeInputState extends State implements ZetaFormFieldSt mainAxisSize: MainAxisSize.min, children: [ if (_showClearButton) - _IconButton( + InputIconButton( icon: widget.rounded ? ZetaIcons.cancel_round : ZetaIcons.cancel_sharp, onTap: reset, disabled: widget.disabled, - size: _iconSize, + size: widget.size, color: _colors.iconSubtle, ), - _IconButton( + InputIconButton( icon: widget.rounded ? ZetaIcons.clock_outline_round : ZetaIcons.clock_outline_sharp, onTap: _pickTime, disabled: widget.disabled, - size: _iconSize, + size: widget.size, color: _colors.iconDefault, ), ], @@ -280,49 +270,3 @@ class ZetaTimeInputState extends State implements ZetaFormFieldSt ); } } - -class _IconButton extends StatelessWidget { - const _IconButton({ - required this.icon, - required this.onTap, - required this.disabled, - required this.size, - required this.color, - }); - - final IconData icon; - final VoidCallback onTap; - final bool disabled; - final double size; - final Color color; - - @override - Widget build(BuildContext context) { - final colors = Zeta.of(context).colors; - - return IconButton( - padding: EdgeInsets.all(size / 2), - constraints: BoxConstraints( - maxHeight: size * 2, - maxWidth: size * 2, - minHeight: size * 2, - minWidth: size * 2, - ), - color: !disabled ? color : colors.iconDisabled, - onPressed: disabled ? null : onTap, - iconSize: size, - icon: Icon(icon), - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('icon', icon)) - ..add(ObjectFlagProperty.has('onTap', onTap)) - ..add(DiagnosticsProperty('disabled', disabled)) - ..add(DoubleProperty('size', size)) - ..add(ColorProperty('color', color)); - } -}