From d9bfe19f7b32daac5963e628e7d3499d07ff17c1 Mon Sep 17 00:00:00 2001 From: mikecoomber <58986130+mikecoomber@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:28:33 +0100 Subject: [PATCH] fix(UX-1006): Gave dropdown functionality to dropdown buttons in button groups (#115) fix: Changed the functionality of dropdown sizes. Mini now sets the width of the menu to its largest child, and standard will set the minimum width to the width of the dropdown's parent. fix: Removed the default offest from dropdown and added an offest. --- .../lib/pages/components/button_example.dart | 21 +- .../pages/components/dropdown_example.dart | 6 + .../pages/components/button_widgetbook.dart | 58 ++-- .../components/button_group/button_group.dart | 229 +++++++----- lib/src/components/dropdown/dropdown.dart | 328 +++++++++--------- .../dropdown/dropdown_controller.dart | 18 + .../components/select_input/select_input.dart | 2 + lib/src/utils/enums.dart | 21 ++ 8 files changed, 406 insertions(+), 277 deletions(-) create mode 100644 lib/src/components/dropdown/dropdown_controller.dart 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, +}