diff --git a/example/lib/pages/components/button_example.dart b/example/lib/pages/components/button_example.dart index 48748d9f..b6c06df9 100644 --- a/example/lib/pages/components/button_example.dart +++ b/example/lib/pages/components/button_example.dart @@ -202,14 +202,18 @@ class _ButtonExampleState extends State { label: "Label", ), ZetaGroupButton.dropdown( - onPressed: () {}, + onChange: print, label: "Label", - dropdown: SizedBox(height: 100, width: 100), + items: [ + ZetaDropdownItem(value: 'Item 1'), + ZetaDropdownItem(value: 'Item 2'), + ], ), ]), ZetaButtonGroup( isLarge: true, rounded: true, + isInverse: true, buttons: [ ZetaGroupButton.icon( icon: ZetaIcons.star_round, @@ -217,9 +221,18 @@ class _ButtonExampleState extends State { label: "Label", ), ZetaGroupButton.dropdown( - onPressed: () {}, + icon: ZetaIcons.star, + onChange: (item) { + print(item); + }, label: "Label", - dropdown: SizedBox(height: 100, width: 100), + items: [ + ZetaDropdownItem( + value: 'Item 1', + icon: Icon(ZetaIcons.star_half), + ), + ZetaDropdownItem(value: 'Item 2'), + ], ), ZetaGroupButton.icon( icon: ZetaIcons.star_round, diff --git a/example/lib/pages/components/dropdown_example.dart b/example/lib/pages/components/dropdown_example.dart index 87e5250d..37967d48 100644 --- a/example/lib/pages/components/dropdown_example.dart +++ b/example/lib/pages/components/dropdown_example.dart @@ -40,6 +40,12 @@ class _DropdownExampleState extends State { value: selectedItem, type: ZetaDropdownMenuType.checkbox, ), + ZetaDropdown( + items: items, + value: selectedItem, + size: ZetaDropdownSize.mini, + onChange: (_) {}, + ), Center( child: Column( mainAxisSize: MainAxisSize.min, diff --git a/example/widgetbook/pages/components/button_widgetbook.dart b/example/widgetbook/pages/components/button_widgetbook.dart index 03f7c30f..8b47a75c 100644 --- a/example/widgetbook/pages/components/button_widgetbook.dart +++ b/example/widgetbook/pages/components/button_widgetbook.dart @@ -65,31 +65,39 @@ Widget buttonGroupUseCase(BuildContext context) { final onPressed = disabledKnob(context) ? null : () {}; return WidgetbookTestWidget( - widget: ZetaButtonGroup( - isLarge: context.knobs.boolean(label: 'Large'), - rounded: rounded, - isInverse: context.knobs.boolean(label: 'Inverse'), - buttons: [ - ZetaGroupButton( - label: context.knobs.string(label: 'Button 1 Title', initialValue: 'Button'), - onPressed: onPressed, - icon: iconKnob(context, name: 'Button 1 Icon', nullable: true, initial: null, rounded: rounded), - dropdown: context.knobs.boolean(label: 'Button 1 Dropdown') ? Container() : null, - ), - ZetaGroupButton( - label: context.knobs.string(label: 'Button 2 Title'), - onPressed: onPressed, - icon: iconKnob(context, name: 'Button 2 Icon', nullable: true, initial: null, rounded: rounded), - dropdown: context.knobs.boolean(label: 'Button 2 Dropdown') ? Container() : null, - ), - ZetaGroupButton( - label: context.knobs.string(label: 'Button 3 Title'), - onPressed: onPressed, - icon: iconKnob(context, name: 'Button 3 Icon', nullable: true, initial: null, rounded: rounded), - dropdown: context.knobs.boolean(label: 'Button 3 Dropdown') ? Container() : null, - ) - ], - )); + widget: ZetaButtonGroup( + isLarge: context.knobs.boolean(label: 'Large'), + rounded: rounded, + isInverse: context.knobs.boolean(label: 'Inverse'), + buttons: [ + ZetaGroupButton( + label: context.knobs.string(label: 'Button 1 Title', initialValue: 'Button'), + onPressed: onPressed, + icon: iconKnob(context, name: 'Button 1 Icon', nullable: true, initial: null, rounded: rounded), + ), + ZetaGroupButton.dropdown( + label: context.knobs.string(label: 'Button 2 Title'), + onChange: disabledKnob(context) ? null : (_) {}, + icon: iconKnob(context, name: 'Button 2 Icon', nullable: true, initial: null, rounded: rounded), + items: [ + ZetaDropdownItem( + value: 'Item 1', + icon: Icon(ZetaIcons.star), + ), + ZetaDropdownItem( + value: 'Item 2', + icon: Icon(ZetaIcons.star_half), + ), + ], + ), + ZetaGroupButton( + label: context.knobs.string(label: 'Button 3 Title'), + onPressed: onPressed, + icon: iconKnob(context, name: 'Button 3 Icon', nullable: true, initial: null, rounded: rounded), + ) + ], + ), + ); } Widget floatingActionButtonUseCase(BuildContext context) => WidgetbookTestWidget( diff --git a/lib/src/components/button_group/button_group.dart b/lib/src/components/button_group/button_group.dart index ec252b94..f9849cf8 100644 --- a/lib/src/components/button_group/button_group.dart +++ b/lib/src/components/button_group/button_group.dart @@ -69,55 +69,63 @@ class ZetaGroupButton extends ZetaStatefulWidget { /// Public Constructor for [ZetaGroupButton] const ZetaGroupButton({ super.key, + this.onPressed, this.label, this.icon, - this.onPressed, - this.dropdown, super.rounded, }) : isFinal = false, isInitial = false, isInverse = false, - isLarge = true; + isLarge = true, + initialValue = null, + items = null, + onChange = null; /// Private constructor const ZetaGroupButton._({ - super.key, - super.rounded, - this.label, - this.icon, - this.onPressed, - this.dropdown, required this.isFinal, required this.isInitial, required this.isInverse, required this.isLarge, + this.onChange, + this.label, + this.initialValue, + this.icon, + this.onPressed, + this.items, + super.key, + super.rounded, }); /// Constructs dropdown group button const ZetaGroupButton.dropdown({ - super.key, - super.rounded, - required this.onPressed, - required this.dropdown, + required this.items, + this.onChange, + this.initialValue, this.icon, this.label, + super.key, + super.rounded, }) : isFinal = false, isInitial = false, isInverse = false, - isLarge = true; + isLarge = true, + onPressed = null; ///Constructs group button with icon const ZetaGroupButton.icon({ super.key, super.rounded, required this.icon, - this.dropdown, this.onPressed, this.label, }) : isFinal = false, isInitial = false, isInverse = false, - isLarge = true; + isLarge = true, + items = null, + onChange = null, + initialValue = null; /// Label for [ZetaGroupButton]. final String? label; @@ -132,8 +140,15 @@ class ZetaGroupButton extends ZetaStatefulWidget { /// {@endtemplate} final VoidCallback? onPressed; - /// Content of dropdown. - final Widget? dropdown; + /// {@macro dropdown-items} + final List>? items; + + /// {@macro dropdown-on-change} + /// {@macro zeta-widget-change-disable} + final void Function(ZetaDropdownItem selectedItem)? onChange; + + /// {@macro dropdown-value} + final dynamic initialValue; ///If [ZetaGroupButton] is large. final bool isLarge; @@ -163,8 +178,10 @@ class ZetaGroupButton extends ZetaStatefulWidget { label: label, icon: icon, onPressed: onPressed, - dropdown: dropdown, + items: items, + onChange: onChange, isFinal: isFinal ?? this.isFinal, + initialValue: initialValue, isInitial: isInitial ?? this.isInitial, isLarge: isLarge ?? this.isLarge, rounded: rounded ?? this.rounded, @@ -182,23 +199,20 @@ class ZetaGroupButton extends ZetaStatefulWidget { ..add(DiagnosticsProperty('isInitial', isInitial)) ..add(DiagnosticsProperty('isLarge', isLarge)) ..add(DiagnosticsProperty('isFinal', isFinal)) - ..add(DiagnosticsProperty('isInverse', isInverse)); + ..add(DiagnosticsProperty('isInverse', isInverse)) + ..add(IterableProperty>('dropdownItems', items)) + ..add(ObjectFlagProperty selectedItem)?>.has('onChange', onChange)) + ..add(DiagnosticsProperty('initialValue', initialValue)); } } class _ZetaGroupButtonState extends State { - late WidgetStatesController controller; + WidgetStatesController controller = WidgetStatesController(); @override - void initState() { - super.initState(); - controller = WidgetStatesController(); - controller.addListener(() { - if (!controller.value.contains(WidgetState.disabled) && context.mounted && mounted) { - // TODO(UX-1005): setState causing exception when going from disabled to enabled. - setState(() {}); - } - }); + void dispose() { + super.dispose(); + controller.dispose(); } @override @@ -209,65 +223,18 @@ class _ZetaGroupButtonState extends State { } } - @override - Widget build(BuildContext context) { - final colors = Zeta.of(context).colors; - final rounded = context.rounded; - final borderType = rounded ? ZetaWidgetBorder.rounded : ZetaWidgetBorder.sharp; - - final BorderSide borderSide = _getBorderSide(controller.value, colors, false); - - return Container( - decoration: BoxDecoration( - border: Border( - top: borderSide, - left: borderSide, - bottom: borderSide, - right: controller.value.contains(WidgetState.focused) - ? BorderSide(color: colors.blue.shade50, width: 2) - : (widget.isFinal) - ? borderSide - : BorderSide.none, - ), - borderRadius: _getRadius(borderType), - ), - padding: EdgeInsets.zero, - child: FilledButton( - statesController: controller, - onPressed: widget.onPressed, // TODO(UX-1006): Dropdown - style: getStyle(borderType, colors), - child: SelectionContainer.disabled( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.icon != null) Icon(widget.icon, size: ZetaSpacing.xl_1), - Text(widget.label ?? '', style: ZetaTextStyles.labelMedium), - if (widget.dropdown != null) // TODO(UX-1006): Dropdown - Icon( - rounded ? ZetaIcons.expand_more_round : ZetaIcons.expand_more_sharp, - size: ZetaSpacing.xl_1, - ), - ].divide(const SizedBox(width: ZetaSpacing.minimum)).toList(), - ).paddingAll(_padding), - ), - ), - ); - } - double get _padding => widget.isLarge ? ZetaSpacing.large : ZetaSpacing.medium; BorderSide _getBorderSide( - Set states, ZetaColors colors, bool finalButton, ) { - if (states.contains(WidgetState.focused)) { + if (controller.value.contains(WidgetState.focused)) { return BorderSide(color: colors.blue.shade50, width: ZetaSpacingBase.x0_5); } - if (states.contains(WidgetState.disabled)) { + if (controller.value.contains(WidgetState.disabled)) { return BorderSide(color: colors.cool.shade40); } - if (widget.isInverse) return BorderSide(color: colors.black); return BorderSide( color: finalButton ? colors.borderDefault : colors.borderSubtle, ); @@ -301,10 +268,10 @@ class _ZetaGroupButtonState extends State { return colors.surfaceDisabled; } if (states.contains(WidgetState.pressed)) { - return colors.primary.shade10; + return widget.isInverse ? colors.cool.shade100 : colors.primary.shade10; } if (states.contains(WidgetState.hovered)) { - return colors.cool.shade20; + return widget.isInverse ? colors.cool.shade90 : colors.cool.shade20; } if (widget.isInverse) return colors.cool.shade100; @@ -322,6 +289,106 @@ class _ZetaGroupButtonState extends State { ); } + Widget _getButton( + VoidCallback? onPressed, { + bool dropdownOpen = false, + ZetaDropdownItem? selectedItem, + }) { + final colors = Zeta.of(context).colors; + final rounded = context.rounded; + final borderType = rounded ? ZetaWidgetBorder.rounded : ZetaWidgetBorder.sharp; + final BorderSide borderSide = _getBorderSide(colors, false); + + late final IconData dropdownIcon; + + if (!dropdownOpen || onPressed == null) { + dropdownIcon = rounded ? ZetaIcons.expand_more_round : ZetaIcons.expand_more_sharp; + } else { + dropdownIcon = rounded ? ZetaIcons.expand_less_round : ZetaIcons.expand_less_sharp; + } + + const iconSize = ZetaSpacing.xl_1; + + Widget? leadingIcon; + if (selectedItem?.icon != null) { + leadingIcon = IconTheme( + data: const IconThemeData( + size: iconSize, + ), + child: selectedItem!.icon!, + ); + } else if (selectedItem == null && widget.icon != null) { + leadingIcon = Icon( + widget.icon, + size: iconSize, + ); + } + + return Container( + decoration: BoxDecoration( + border: Border( + top: borderSide, + left: borderSide, + bottom: borderSide, + right: controller.value.contains(WidgetState.focused) + ? BorderSide(color: colors.blue.shade50, width: 2) + : (widget.isFinal) + ? borderSide + : BorderSide.none, + ), + borderRadius: _getRadius(borderType), + ), + padding: EdgeInsets.zero, + child: FilledButton( + statesController: controller, + onPressed: onPressed, + style: getStyle(borderType, colors), + child: SelectionContainer.disabled( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + leadingIcon ?? const SizedBox(), + Text(selectedItem?.label ?? widget.label ?? '', style: ZetaTextStyles.labelMedium), + if (widget.items != null) + Icon( + dropdownIcon, + size: ZetaSpacing.xl_1, + ), + ].divide(const SizedBox(width: ZetaSpacing.minimum)).toList(), + ).paddingAll(_padding), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final rounded = context.rounded; + + late final Widget child; + + if (widget.items != null) { + child = ZetaDropdown( + items: widget.items!, + onChange: widget.onChange, + onDismissed: () => setState(() {}), + value: widget.initialValue, + rounded: rounded, + builder: (context, selectedItem, controller) { + return _getButton( + widget.onChange != null ? controller.toggle : null, + dropdownOpen: controller.isOpen, + selectedItem: selectedItem, + ); + }, + ); + } else { + child = _getButton(widget.onPressed); + } + + return child; + } + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); diff --git a/lib/src/components/dropdown/dropdown.dart b/lib/src/components/dropdown/dropdown.dart index 2a124a48..a61c8d21 100644 --- a/lib/src/components/dropdown/dropdown.dart +++ b/lib/src/components/dropdown/dropdown.dart @@ -4,49 +4,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../../../zeta_flutter.dart'; - -/// Sets the type of a [ZetaDropdown] -enum ZetaDropdownMenuType { - /// No leading elements before each item unless an icon is given to the [ZetaDropdownItem] - standard, - - /// Displays a [ZetaCheckbox] before each item. - checkbox, - - /// Displays a [ZetaRadio] before each item. - radio -} - -/// Used to set the size of a [ZetaDropdown] -enum ZetaDropdownSize { - /// Initial width of 320dp. - standard, - - /// Initial width of 120dp. - mini, -} - -/// A class for controlling a [ZetaDropdown] -/// -/// Can be acquired 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(); -} +import 'dropdown_controller.dart'; class _DropdownControllerImpl implements ZetaDropdownController { - _DropdownControllerImpl({required this.overlayPortalController}); + _DropdownControllerImpl({required this.overlayPortalController, required this.toggleDropdown}); final OverlayPortalController overlayPortalController; + final VoidCallback toggleDropdown; @override bool get isOpen => overlayPortalController.isShowing; @@ -58,7 +22,7 @@ class _DropdownControllerImpl implements ZetaDropdownController { void open() => overlayPortalController.show(); @override - void toggle() => overlayPortalController.toggle(); + void toggle() => toggleDropdown(); } /// An item used in a [ZetaDropdown] or a [ZetaSelectInput]. @@ -94,23 +58,30 @@ class ZetaDropdown extends ZetaStatefulWidget { this.value, this.type = ZetaDropdownMenuType.standard, this.size = ZetaDropdownSize.standard, + this.offset = Offset.zero, this.builder, this.onDismissed, super.key, super.rounded, }); + /// {@template dropdown-items} /// The items displayed in the dropdown. + /// {@endtemplate} final List> items; + /// {@template dropdown-value} /// The value of the selected item. /// - /// If no [ZetaDropdownItem] in [items] has a matching value, the first item in [items] will be set as the selected item. + /// A [ZetaDropdownItem] in [items] must have a corresponding value for this to be set. + /// {@endtemplate} final T? value; + /// {@template dropdown-on-change} /// Called with the selected value whenever the dropdown is changed. /// /// {@macro zeta-widget-change-disable} + /// {@endtemplate} final ValueSetter>? onChange; /// Called when the dropdown is dismissed. @@ -126,6 +97,9 @@ class ZetaDropdown extends ZetaStatefulWidget { /// Defaults to [ZetaDropdownSize.mini] final ZetaDropdownSize size; + /// The offset of the dropdown menu from its parent. + final Offset offset; + /// 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. @@ -155,12 +129,13 @@ class ZetaDropdown extends ZetaStatefulWidget { ZetaDropdownItem? selectedItem, ZetaDropdownController controller, )?>.has('builder', builder), - ); + ) + ..add(DiagnosticsProperty('offset', offset)); } } /// Enum possible menu positions -enum MenuPosition { +enum _MenuPosition { /// IF Menu is rendered above up, @@ -170,13 +145,15 @@ enum MenuPosition { /// The state for a [ZetaDropdown] class ZetaDropDownState extends State> { - final _DropdownControllerImpl _dropdownController = _DropdownControllerImpl( - overlayPortalController: OverlayPortalController(), - ); + late final _DropdownControllerImpl _dropdownController; + final OverlayPortalController _overlayPortalController = OverlayPortalController(); + final _link = LayerLink(); final _menuKey = GlobalKey(); final _childKey = GlobalKey(); - MenuPosition _menuPosition = MenuPosition.down; + _MenuPosition _menuPosition = _MenuPosition.down; + + double? _menuSize; ZetaDropdownItem? _selectedItem; @@ -187,6 +164,13 @@ class ZetaDropDownState extends State> { @override void initState() { super.initState(); + _dropdownController = _DropdownControllerImpl( + overlayPortalController: _overlayPortalController, + toggleDropdown: _toggleDropdown, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + _setMenuSize(); + }); _setSelectedItem(); } @@ -199,7 +183,12 @@ class ZetaDropDownState extends State> { if (oldWidget.value != widget.value) { setState(_setSelectedItem); } - if (widget.onChange == null) { + if (oldWidget.size != widget.size) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _setMenuSize(); + }); + } + if (oldWidget.onChange != widget.onChange && widget.onChange == null) { unawaited( Future.delayed(Duration.zero).then( (value) => _dropdownController.close(), @@ -208,6 +197,26 @@ class ZetaDropDownState extends State> { } } + void _toggleDropdown() { + /// Version 1 : Calculate if overflow happens based on using calculations from sizes. + final height = MediaQuery.of(context).size.height; + final headerRenderBox = _childKey.currentContext!.findRenderObject()! as RenderBox; + final dropdownItemHeight = headerRenderBox.size.height; + + /// Calculate if overflow can happen + final headerPosY = _headerPos.dy; + + setState(() { + if (headerPosY + (dropdownItemHeight * (widget.items.length + 1)) > height) { + _menuPosition = _MenuPosition.up; + } else { + _menuPosition = _MenuPosition.down; + } + }); + + _overlayPortalController.toggle(); + } + /// Return position of header Offset get _headerPos { final headerBox = _childKey.currentContext!.findRenderObject()! as RenderBox; @@ -224,6 +233,15 @@ class ZetaDropDownState extends State> { } } + void _setMenuSize() { + _dropdownController.close(); + if (widget.size == ZetaDropdownSize.mini) { + _menuSize = null; + } else { + _menuSize = _childKey.currentContext?.size?.width ?? 0; + } + } + // Returns if click event position is within the header. bool _isInHeader( Offset headerPosition, @@ -247,94 +265,72 @@ class ZetaDropDownState extends State> { ); } else { child = _DropdownItem( - onPress: widget.onChange != null ? _onTap : null, + onPress: widget.onChange != null ? _toggleDropdown : null, value: _selectedItem ?? widget.items.first, allocateLeadingSpace: widget.type == ZetaDropdownMenuType.standard && _selectedItem?.icon != null, key: _childKey, ); + if (widget.size == ZetaDropdownSize.mini) { + child = IntrinsicWidth( + child: child, + ); + } } - return SizedBox( - width: _size, - child: CompositedTransformTarget( - link: _link, - child: OverlayPortal( - controller: _dropdownController.overlayPortalController, - overlayChildBuilder: (BuildContext context) { - return CompositedTransformFollower( - link: _link, - targetAnchor: _menuPosition == MenuPosition.up - ? 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 * -1), - child: Align( - alignment: - _menuPosition == MenuPosition.up ? AlignmentDirectional.bottomStart : AlignmentDirectional.topStart, - child: TapRegion( - onTapOutside: (event) { - final headerBox = _childKey.currentContext!.findRenderObject()! as RenderBox; - - final headerPosition = headerBox.localToGlobal(Offset.zero); - final inHeader = _isInHeader( - headerPosition, - headerBox.size, - event.position, - ); - if (!inHeader) { - _dropdownController.close(); - widget.onDismissed?.call(); - } + return CompositedTransformTarget( + link: _link, + child: OverlayPortal( + controller: _dropdownController.overlayPortalController, + overlayChildBuilder: (BuildContext context) { + return CompositedTransformFollower( + link: _link, + targetAnchor: _menuPosition == _MenuPosition.up + ? Alignment.topLeft + : Alignment.bottomLeft, // Align overlay dropdown in its correct position + followerAnchor: _menuPosition == _MenuPosition.up ? Alignment.bottomLeft : Alignment.topLeft, + offset: widget.offset, + child: Align( + alignment: + _menuPosition == _MenuPosition.up ? AlignmentDirectional.bottomStart : AlignmentDirectional.topStart, + child: TapRegion( + onTapOutside: (event) { + final headerBox = _childKey.currentContext!.findRenderObject()! as RenderBox; + + final headerPosition = headerBox.localToGlobal(Offset.zero); + final inHeader = _isInHeader( + headerPosition, + headerBox.size, + event.position, + ); + if (!inHeader) { + _dropdownController.close(); + widget.onDismissed?.call(); + } + }, + child: _ZetaDropDownMenu( + items: widget.items, + selected: _selectedItem?.value, + allocateLeadingSpace: _allocateLeadingSpace, + menuSize: _menuSize, + key: _menuKey, + menuType: widget.type, + onSelected: (item) { + setState(() { + _selectedItem = item; + }); + widget.onChange?.call(item); + _dropdownController.close(); }, - child: _ZetaDropDownMenu( - items: widget.items, - selected: _selectedItem?.value, - allocateLeadingSpace: _allocateLeadingSpace, - width: _size, - key: _menuKey, - menuType: widget.type, - onSelected: (item) { - setState(() { - _selectedItem = item; - }); - widget.onChange?.call(item); - _dropdownController.close(); - }, - ), ), ), - ); - }, - child: child, - ), + ), + ); + }, + child: child, ), ); } - double get _size => widget.size == ZetaDropdownSize.mini ? 120 : 320; - - void _onTap() { - /// Version 1 : Calculate if overflow happens based on using calculations from sizes. - final height = MediaQuery.of(context).size.height; - final headerRenderBox = _childKey.currentContext!.findRenderObject()! as RenderBox; - final dropdownItemHeight = headerRenderBox.size.height; - - /// Calculate if overflow can happen - final headerPosY = _headerPos.dy; - - if (headerPosY + (dropdownItemHeight * (widget.items.length + 1)) > height) { - setState(() { - _menuPosition = MenuPosition.up; - }); - } else { - setState(() { - _menuPosition = MenuPosition.down; - }); - } - - _dropdownController.toggle(); - } - @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -431,35 +427,30 @@ class _DropdownItemState extends State<_DropdownItem> { ); } - return ConstrainedBox( - constraints: const BoxConstraints(maxHeight: ZetaSpacing.xl_6), - child: DefaultTextStyle( - style: ZetaTextStyles.bodyMedium, - child: OutlinedButton( - key: widget.itemKey, - onPressed: widget.onPress, - statesController: controller, - style: _getStyle(colors), + return DefaultTextStyle( + style: ZetaTextStyles.bodyMedium, + child: OutlinedButton( + key: widget.itemKey, + onPressed: widget.onPress, + statesController: controller, + style: _getStyle(colors), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: ZetaSpacingBase.x2_5, + horizontal: ZetaSpacing.medium, + ), child: Row( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(width: ZetaSpacing.medium), if (leading != null) leading, Expanded( - child: Padding( - padding: const EdgeInsets.only(right: ZetaSpacing.small), - child: Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: Text( - widget.value.label, - ), - ), - ), + child: Text( + widget.value.label, ), ), ], - ).paddingVertical(ZetaSpacingBase.x2_5), + ), ), ), ); @@ -548,7 +539,7 @@ class _ZetaDropDownMenu extends ZetaStatefulWidget { required this.onSelected, required this.selected, required this.allocateLeadingSpace, - this.width, + required this.menuSize, this.menuType = ZetaDropdownMenuType.standard, super.key, }); @@ -563,8 +554,8 @@ class _ZetaDropDownMenu extends ZetaStatefulWidget { final T? selected; - /// Width for menu - final double? width; + /// Width for menu. If set to null the menu will shrink to the width of its widest child. + final double? menuSize; /// If items have checkboxes, the type of that checkbox. final ZetaDropdownMenuType menuType; @@ -575,7 +566,7 @@ class _ZetaDropDownMenu extends ZetaStatefulWidget { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(DoubleProperty('width', width)) + ..add(DoubleProperty('width', menuSize)) ..add(EnumProperty('boxType', menuType)) ..add(IterableProperty>('items', items)) ..add(ObjectFlagProperty>>.has('onSelected', onSelected)) @@ -603,25 +594,28 @@ class _ZetaDropDownMenuState extends State<_ZetaDropDownMenu> { ), ], ), - width: widget.width, - child: Builder( - builder: (BuildContext bcontext) { - return Column( - mainAxisSize: MainAxisSize.min, - children: widget.items - .map((item) { - return _DropdownItem( - value: item, - onPress: () => widget.onSelected(item), - selected: item.value == widget.selected, - allocateLeadingSpace: widget.allocateLeadingSpace, - menuType: widget.menuType, - ); - }) - .divide(const SizedBox(height: ZetaSpacing.minimum)) - .toList(), - ); - }, + constraints: widget.menuSize != null + ? BoxConstraints( + minWidth: widget.menuSize!, + ) + : null, + child: IntrinsicWidth( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: widget.items + .map((item) { + return _DropdownItem( + value: item, + onPress: () => widget.onSelected(item), + selected: item.value == widget.selected, + allocateLeadingSpace: widget.allocateLeadingSpace, + menuType: widget.menuType, + ); + }) + .divide(const SizedBox(height: ZetaSpacing.minimum)) + .toList(), + ), ), ); } diff --git a/lib/src/components/dropdown/dropdown_controller.dart b/lib/src/components/dropdown/dropdown_controller.dart new file mode 100644 index 00000000..3ee2e6d1 --- /dev/null +++ b/lib/src/components/dropdown/dropdown_controller.dart @@ -0,0 +1,18 @@ +import '../../../zeta_flutter.dart'; + +/// A class for controlling a [ZetaDropdown] +/// +/// Can be acquired 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(); +} diff --git a/lib/src/components/select_input/select_input.dart b/lib/src/components/select_input/select_input.dart index 6ea79a65..9e847a51 100644 --- a/lib/src/components/select_input/select_input.dart +++ b/lib/src/components/select_input/select_input.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import '../../../zeta_flutter.dart'; import '../../interfaces/form_field.dart'; import '../buttons/input_icon_button.dart'; +import '../dropdown/dropdown_controller.dart'; /// Class for [ZetaSelectInput] class ZetaSelectInput extends ZetaFormField { @@ -157,6 +158,7 @@ class _ZetaSelectInputState extends State> { onChange: !widget.disabled ? _onDropdownChanged : null, key: _dropdownKey, value: _selectedItem?.value, + offset: const Offset(0, ZetaSpacing.xl_1 * -1), onDismissed: () => setState(() {}), builder: (context, _, controller) { return ZetaTextInput( diff --git a/lib/src/utils/enums.dart b/lib/src/utils/enums.dart index 6fa1f6e2..7bf2157e 100644 --- a/lib/src/utils/enums.dart +++ b/lib/src/utils/enums.dart @@ -53,3 +53,24 @@ enum ZetaFormFieldRequirement { /// An optional form field. optional, } + +/// Sets the type of a [ZetaDropdown] +enum ZetaDropdownMenuType { + /// No leading elements before each item unless an icon is given to the [ZetaDropdownItem] + standard, + + /// Displays a [ZetaCheckbox] before each item. + checkbox, + + /// Displays a [ZetaRadio] before each item. + radio +} + +/// Used to set the size of a [ZetaDropdown] +enum ZetaDropdownSize { + /// The minimum width of the dropdown menu is set to the width of the parent. + standard, + + /// The width of the dropdown menu wraps the largest of its children. + mini, +}