diff --git a/example/lib/widgets/decorated_dropdown.dart b/example/lib/widgets/decorated_dropdown.dart index 8610b8d..67becdb 100644 --- a/example/lib/widgets/decorated_dropdown.dart +++ b/example/lib/widgets/decorated_dropdown.dart @@ -21,7 +21,7 @@ class DecoratedDropdown extends StatelessWidget { onChanged: (value) { log('DecoratedDropdown onChanged value: $value'); }, - headerBuilder: (context, selectedItem) { + headerBuilder: (context, selectedItem, enabled) { return Text( selectedItem.toString(), style: const TextStyle( diff --git a/lib/custom_dropdown.dart b/lib/custom_dropdown.dart index 95da054..f03a7fa 100644 --- a/lib/custom_dropdown.dart +++ b/lib/custom_dropdown.dart @@ -9,6 +9,7 @@ export 'custom_dropdown.dart'; // models part 'models/custom_dropdown_decoration.dart'; part 'models/custom_dropdown_list_filter.dart'; +part 'models/disabled_decoration.dart'; part 'models/list_item_decoration.dart'; part 'models/controllers.dart'; part 'models/search_field_decoration.dart'; @@ -146,6 +147,19 @@ class CustomDropdown extends StatefulWidget { /// Contain sub-decorations [SearchFieldDecoration], [ListItemDecoration] and [ScrollbarThemeData]. final CustomDropdownDecoration? decoration; + /// [CustomDropdown] enabled/disabled state. + /// If disabled, you can not open the dropdown. + final bool enabled; + + /// [CustomDropdown] disabled decoration. + /// + /// Note: Only applicable if dropdown is disabled. + final CustomDropdownDisabledDecoration? disabledDecoration; + + /// [CustomDropdown] will close on tap Clear filter for all search + /// and searchRequest constructors + final bool closeDropDownOnClearFilterSearch; + /// The [overlayController] allows you to explicitly handle the [CustomDropdown] overlay states (show/hide). final OverlayPortalController? overlayController; @@ -190,6 +204,8 @@ class CustomDropdown extends StatefulWidget { this.canCloseOutsideBounds = true, this.hideSelectedFieldWhenExpanded = false, this.excludeSelected = true, + this.enabled = true, + this.disabledDecoration, }) : assert( initialItem == null || controller == null, 'Only one of initialItem or controller can be specified at a time', @@ -216,6 +232,7 @@ class CustomDropdown extends StatefulWidget { listValidator = null, headerListBuilder = null, searchRequestLoadingIndicator = null, + closeDropDownOnClearFilterSearch = false, multiSelectController = null; CustomDropdown.search({ @@ -246,6 +263,9 @@ class CustomDropdown extends StatefulWidget { this.excludeSelected = true, this.canCloseOutsideBounds = true, this.hideSelectedFieldWhenExpanded = false, + this.enabled = true, + this.disabledDecoration, + this.closeDropDownOnClearFilterSearch = false, }) : assert( initialItem == null || controller == null, 'Only one of initialItem or controller can be specified at a time', @@ -302,6 +322,9 @@ class CustomDropdown extends StatefulWidget { this.excludeSelected = true, this.canCloseOutsideBounds = true, this.hideSelectedFieldWhenExpanded = false, + this.enabled = true, + this.disabledDecoration, + this.closeDropDownOnClearFilterSearch = false, }) : assert( initialItem == null || controller == null, 'Only one of initialItem or controller can be specified at a time', @@ -339,6 +362,8 @@ class CustomDropdown extends StatefulWidget { this.expandedHeaderPadding, this.itemsListPadding, this.listItemPadding, + this.enabled = true, + this.disabledDecoration, }) : assert( initialItems == null || multiSelectController == null, 'Only one of initialItems or controller can be specified at a time', @@ -367,7 +392,8 @@ class CustomDropdown extends StatefulWidget { futureRequestDelay = null, noResultFoundBuilder = null, searchHintText = null, - searchRequestLoadingIndicator = null; + searchRequestLoadingIndicator = null, + closeDropDownOnClearFilterSearch = false; CustomDropdown.multiSelectSearch({ super.key, @@ -397,6 +423,9 @@ class CustomDropdown extends StatefulWidget { this.expandedHeaderPadding, this.itemsListPadding, this.listItemPadding, + this.enabled = true, + this.disabledDecoration, + this.closeDropDownOnClearFilterSearch = false, }) : assert( initialItems == null || multiSelectController == null, 'Only one of initialItems or controller can be specified at a time', @@ -455,6 +484,9 @@ class CustomDropdown extends StatefulWidget { this.listItemPadding, this.canCloseOutsideBounds = true, this.hideSelectedFieldWhenExpanded = false, + this.enabled = true, + this.disabledDecoration, + this.closeDropDownOnClearFilterSearch = false, }) : assert( initialItems == null || multiSelectController == null, 'Only one of initialItems or controller can be specified at a time', @@ -541,119 +573,142 @@ class _CustomDropdownState extends State> { @override Widget build(BuildContext context) { + final enabled = widget.enabled; final decoration = widget.decoration; + final disabledDecoration = widget.disabledDecoration; final safeHintText = widget.hintText ?? 'Select value'; - return FormField<(T?, List)>( - initialValue: (selectedItemNotifier.value, selectedItemsNotifier.value), - validator: (val) { - if (widget._dropdownType == _DropdownType.singleSelect && - widget.validator != null) { - return widget.validator!(val?.$1); - } - if (widget._dropdownType == _DropdownType.multipleSelect && - widget.listValidator != null) { - return widget.listValidator!(val?.$2 ?? []); - } - return null; - }, - builder: (formFieldState) { - _formFieldState = formFieldState; - return InputDecorator( - decoration: InputDecoration( - errorStyle: decoration?.errorStyle ?? _defaultErrorStyle, - errorText: formFieldState.errorText, - border: InputBorder.none, - contentPadding: EdgeInsets.zero, - ), - child: _OverlayBuilder( - overlayPortalController: widget.overlayController, - visibility: widget.visibility, - overlay: (size, hideCallback) { - return _DropdownOverlay( - onItemSelect: (T value) { - switch (widget._dropdownType) { - case _DropdownType.singleSelect: - selectedItemNotifier.value = value; - case _DropdownType.multipleSelect: - final currentVal = selectedItemsNotifier.value.toList(); - if (currentVal.contains(value)) { - currentVal.remove(value); - } else { - currentVal.add(value); - } - selectedItemsNotifier.value = currentVal; - } - }, - noResultFoundText: - widget.noResultFoundText ?? 'No result found.', - noResultFoundBuilder: widget.noResultFoundBuilder, - items: widget.items ?? [], - itemsScrollCtrl: widget.itemsScrollController, - selectedItemNotifier: selectedItemNotifier, - selectedItemsNotifier: selectedItemsNotifier, - size: size, - listItemBuilder: widget.listItemBuilder, - layerLink: layerLink, - hideOverlay: hideCallback, - hintStyle: decoration?.hintStyle, - headerStyle: decoration?.headerStyle, - noResultFoundStyle: decoration?.noResultFoundStyle, - listItemStyle: decoration?.listItemStyle, - headerBuilder: widget.headerBuilder, - headerListBuilder: widget.headerListBuilder, - hintText: safeHintText, - searchHintText: widget.searchHintText ?? 'Search', - hintBuilder: widget.hintBuilder, - decoration: decoration, - overlayHeight: widget.overlayHeight, - excludeSelected: widget.excludeSelected, - canCloseOutsideBounds: widget.canCloseOutsideBounds, - searchType: widget._searchType, - futureRequest: widget.futureRequest, - futureRequestDelay: widget.futureRequestDelay, - hideSelectedFieldWhenOpen: widget.hideSelectedFieldWhenExpanded, - maxLines: widget.maxlines, - headerPadding: widget.expandedHeaderPadding, - itemsListPadding: widget.itemsListPadding, - listItemPadding: widget.listItemPadding, - searchRequestLoadingIndicator: - widget.searchRequestLoadingIndicator, - dropdownType: widget._dropdownType, - ); - }, - child: (showCallback) { - return CompositedTransformTarget( - link: layerLink, - child: _DropDownField( - onTap: showCallback, + return IgnorePointer( + ignoring: !widget.enabled, + child: FormField<(T?, List)>( + initialValue: (selectedItemNotifier.value, selectedItemsNotifier.value), + validator: (val) { + if (widget._dropdownType == _DropdownType.singleSelect && + widget.validator != null) { + return widget.validator!(val?.$1); + } + if (widget._dropdownType == _DropdownType.multipleSelect && + widget.listValidator != null) { + return widget.listValidator!(val?.$2 ?? []); + } + return null; + }, + builder: (formFieldState) { + _formFieldState = formFieldState; + return InputDecorator( + decoration: InputDecoration( + errorStyle: decoration?.errorStyle ?? _defaultErrorStyle, + errorText: formFieldState.errorText, + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + ), + child: _OverlayBuilder( + overlayPortalController: widget.overlayController, + visibility: widget.visibility, + overlay: (size, hideCallback) { + return _DropdownOverlay( + onItemSelect: (T value) { + switch (widget._dropdownType) { + case _DropdownType.singleSelect: + selectedItemNotifier.value = value; + case _DropdownType.multipleSelect: + final currentVal = selectedItemsNotifier.value.toList(); + if (currentVal.contains(value)) { + currentVal.remove(value); + } else { + currentVal.add(value); + } + selectedItemsNotifier.value = currentVal; + } + }, + noResultFoundText: + widget.noResultFoundText ?? 'No result found.', + noResultFoundBuilder: widget.noResultFoundBuilder, + items: widget.items ?? [], + itemsScrollCtrl: widget.itemsScrollController, selectedItemNotifier: selectedItemNotifier, - border: formFieldState.hasError - ? (decoration?.closedErrorBorder ?? _defaultErrorBorder) - : decoration?.closedBorder, - borderRadius: formFieldState.hasError - ? decoration?.closedErrorBorderRadius - : decoration?.closedBorderRadius, - shadow: decoration?.closedShadow, + selectedItemsNotifier: selectedItemsNotifier, + size: size, + listItemBuilder: widget.listItemBuilder, + layerLink: layerLink, + hideOverlay: hideCallback, hintStyle: decoration?.hintStyle, headerStyle: decoration?.headerStyle, - hintText: safeHintText, - hintBuilder: widget.hintBuilder, + noResultFoundStyle: decoration?.noResultFoundStyle, + listItemStyle: decoration?.listItemStyle, headerBuilder: widget.headerBuilder, headerListBuilder: widget.headerListBuilder, - prefixIcon: decoration?.prefixIcon, - suffixIcon: decoration?.closedSuffixIcon, - fillColor: decoration?.closedFillColor, + hintText: safeHintText, + searchHintText: widget.searchHintText ?? 'Search', + hintBuilder: widget.hintBuilder, + decoration: decoration, + overlayHeight: widget.overlayHeight, + excludeSelected: widget.excludeSelected, + canCloseOutsideBounds: widget.canCloseOutsideBounds, + searchType: widget._searchType, + futureRequest: widget.futureRequest, + futureRequestDelay: widget.futureRequestDelay, + hideSelectedFieldWhenOpen: + widget.hideSelectedFieldWhenExpanded, maxLines: widget.maxlines, - headerPadding: widget.closedHeaderPadding, + headerPadding: widget.expandedHeaderPadding, + itemsListPadding: widget.itemsListPadding, + listItemPadding: widget.listItemPadding, + searchRequestLoadingIndicator: + widget.searchRequestLoadingIndicator, dropdownType: widget._dropdownType, - selectedItemsNotifier: selectedItemsNotifier, - ), - ); - }, - ), - ); - }, + ); + }, + child: (showCallback) { + return CompositedTransformTarget( + link: layerLink, + child: _DropDownField( + onTap: showCallback, + selectedItemNotifier: selectedItemNotifier, + border: formFieldState.hasError + ? (decoration?.closedErrorBorder ?? _defaultErrorBorder) + : enabled + ? decoration?.closedBorder + : disabledDecoration?.border, + borderRadius: formFieldState.hasError + ? decoration?.closedErrorBorderRadius + : enabled + ? decoration?.closedBorderRadius + : disabledDecoration?.borderRadius, + shadow: enabled + ? decoration?.closedShadow + : disabledDecoration?.shadow, + hintStyle: enabled + ? decoration?.hintStyle + : disabledDecoration?.hintStyle, + headerStyle: enabled + ? decoration?.headerStyle + : disabledDecoration?.headerStyle, + hintText: safeHintText, + hintBuilder: widget.hintBuilder, + headerBuilder: widget.headerBuilder, + headerListBuilder: widget.headerListBuilder, + prefixIcon: enabled + ? decoration?.prefixIcon + : disabledDecoration?.prefixIcon, + suffixIcon: enabled + ? decoration?.closedSuffixIcon + : disabledDecoration?.suffixIcon, + fillColor: enabled + ? decoration?.closedFillColor + : disabledDecoration?.fillColor, + maxLines: widget.maxlines, + headerPadding: widget.closedHeaderPadding, + dropdownType: widget._dropdownType, + selectedItemsNotifier: selectedItemsNotifier, + enabled: widget.enabled, + ), + ); + }, + ), + ); + }, + ), ); } } diff --git a/lib/models/disabled_decoration.dart b/lib/models/disabled_decoration.dart new file mode 100644 index 0000000..0f95a04 --- /dev/null +++ b/lib/models/disabled_decoration.dart @@ -0,0 +1,40 @@ +part of '../custom_dropdown.dart'; + +class CustomDropdownDisabledDecoration { + /// [CustomDropdown] field color (disabled state). + /// + /// Default to Color(0xFFF3F3F3). + final Color? fillColor; + + /// [CustomDropdown] box shadow (disabled state). + final List? shadow; + + /// Suffix icon for disabled state of [CustomDropdown]. + final Widget? suffixIcon; + + /// Prefix icon for disabled state of [CustomDropdown]. + final Widget? prefixIcon; + + /// Border for disabled state of [CustomDropdown]. + final BoxBorder? border; + + /// Border radius for disabled state of [CustomDropdown]. + final BorderRadius? borderRadius; + + /// The style to use for the [CustomDropdown] header hint (disabled state). + final TextStyle? hintStyle; + + /// The style to use for the [CustomDropdown] header text (disabled state). + final TextStyle? headerStyle; + + const CustomDropdownDisabledDecoration({ + this.fillColor, + this.shadow, + this.suffixIcon, + this.prefixIcon, + this.border, + this.borderRadius, + this.headerStyle, + this.hintStyle, + }); +} diff --git a/lib/utils/signatures.dart b/lib/utils/signatures.dart index ff50310..351d413 100644 --- a/lib/utils/signatures.dart +++ b/lib/utils/signatures.dart @@ -9,14 +9,17 @@ typedef _ListItemBuilder = Widget Function( typedef _HeaderBuilder = Widget Function( BuildContext context, T selectedItem, + bool enabled, ); typedef _HeaderListBuilder = Widget Function( BuildContext context, List selectedItems, + bool enabled, ); typedef _HintBuilder = Widget Function( BuildContext context, String hint, + bool enabled, ); typedef _NoResultFoundBuilder = Widget Function( BuildContext context, diff --git a/lib/widgets/dropdown_field.dart b/lib/widgets/dropdown_field.dart index 9b13835..1a5bffa 100644 --- a/lib/widgets/dropdown_field.dart +++ b/lib/widgets/dropdown_field.dart @@ -24,6 +24,7 @@ class _DropDownField extends StatefulWidget { final _HeaderListBuilder? headerListBuilder; final _HintBuilder? hintBuilder; final _DropdownType dropdownType; + final bool enabled; final MultiSelectController selectedItemsNotifier; const _DropDownField({ @@ -49,6 +50,7 @@ class _DropDownField extends StatefulWidget { this.prefixIcon, this.suffixIcon, this.headerPadding, + this.enabled = true, }); @override @@ -68,19 +70,19 @@ class _DropDownFieldState extends State<_DropDownField> { Widget hintBuilder(BuildContext context) { return widget.hintBuilder != null - ? widget.hintBuilder!(context, widget.hintText) - : defaultHintBuilder(widget.hintText); + ? widget.hintBuilder!(context, widget.hintText, widget.enabled) + : defaultHintBuilder(widget.hintText, widget.enabled); } Widget headerBuilder(BuildContext context) { return widget.headerBuilder != null - ? widget.headerBuilder!(context, selectedItem as T) + ? widget.headerBuilder!(context, selectedItem as T, widget.enabled) : defaultHeaderBuilder(oneItem: selectedItem); } Widget headerListBuilder(BuildContext context) { return widget.headerListBuilder != null - ? widget.headerListBuilder!(context, selectedItems) + ? widget.headerListBuilder!(context, selectedItems, widget.enabled) : defaultHeaderBuilder(itemList: selectedItems); } @@ -90,14 +92,15 @@ class _DropDownFieldState extends State<_DropDownField> { maxLines: widget.maxLines, overflow: TextOverflow.ellipsis, style: widget.headerStyle ?? - const TextStyle( + TextStyle( fontSize: 16, fontWeight: FontWeight.w500, + color: widget.enabled ? null : Colors.black.withOpacity(.5), ), ); } - Widget defaultHintBuilder(String hint) { + Widget defaultHintBuilder(String hint, bool enabled) { return Text( hint, maxLines: 1, @@ -128,7 +131,10 @@ class _DropDownFieldState extends State<_DropDownField> { child: Container( padding: widget.headerPadding ?? _defaultHeaderPadding, decoration: BoxDecoration( - color: widget.fillColor ?? CustomDropdownDecoration._defaultFillColor, + color: widget.fillColor ?? + (widget.enabled + ? CustomDropdownDecoration._defaultFillColor + : CustomDropdownDecoration._defaultFillColor.withOpacity(.5)), border: widget.border, borderRadius: widget.borderRadius ?? _defaultBorderRadius, boxShadow: widget.shadow, @@ -150,7 +156,14 @@ class _DropDownFieldState extends State<_DropDownField> { }, ), const SizedBox(width: 12), - widget.suffixIcon ?? _defaultOverlayIconDown, + widget.suffixIcon ?? + (widget.enabled + ? _defaultOverlayIconDown + : Icon( + Icons.keyboard_arrow_down_rounded, + color: Colors.black.withOpacity(.5), + size: 20, + )), ], ), ), diff --git a/lib/widgets/dropdown_overlay/dropdown_overlay.dart b/lib/widgets/dropdown_overlay/dropdown_overlay.dart index e8db0c0..e1b76c4 100644 --- a/lib/widgets/dropdown_overlay/dropdown_overlay.dart +++ b/lib/widgets/dropdown_overlay/dropdown_overlay.dart @@ -93,19 +93,19 @@ class _DropdownOverlayState extends State<_DropdownOverlay> { Widget hintBuilder(BuildContext context) { return widget.hintBuilder != null - ? widget.hintBuilder!(context, widget.hintText) + ? widget.hintBuilder!(context, widget.hintText, true) : defaultHintBuilder(context, widget.hintText); } Widget headerBuilder(BuildContext context) { return widget.headerBuilder != null - ? widget.headerBuilder!(context, selectedItem as T) + ? widget.headerBuilder!(context, selectedItem as T, true) : defaultHeaderBuilder(context, item: selectedItem); } Widget headerListBuilder(BuildContext context) { return widget.headerListBuilder != null - ? widget.headerListBuilder!(context, selectedItems) + ? widget.headerListBuilder!(context, selectedItems, true) : defaultHeaderBuilder(context, items: selectedItems); }