├── .github ├── dependabot.yaml ├── images │ ├── banner.jpg │ ├── few_styling.png │ ├── form_field.png │ ├── multi_select.png │ ├── popup_icon.png │ ├── popup_image.png │ ├── search.png │ ├── simple.png │ └── with_separators.png └── workflows │ └── build.yml ├── .gitignore ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── dart_test.yaml ├── melos.yaml ├── packages ├── dropdown_button2 │ ├── .pubignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── example │ │ ├── custom_dropdown_button2.dart │ │ └── example.dart │ ├── lib │ │ ├── dropdown_button2.dart │ │ └── src │ │ │ ├── button_style_data.dart │ │ │ ├── dropdown_button2.dart │ │ │ ├── dropdown_menu.dart │ │ │ ├── dropdown_menu_item.dart │ │ │ ├── dropdown_menu_separators.dart │ │ │ ├── dropdown_route.dart │ │ │ ├── dropdown_style_data.dart │ │ │ ├── enums.dart │ │ │ ├── seperated_sliver_child_builder_delegate.dart │ │ │ └── utils.dart │ ├── pubspec.yaml │ └── test │ │ └── dropdown_button2_test.dart └── dropdown_button2_test │ ├── analysis_options.yaml │ ├── assets │ ├── fonts │ │ ├── LICENSE.txt │ │ └── Roboto-Regular.ttf │ └── images │ │ └── city.jpg │ ├── lib │ ├── examples.dart │ └── src │ │ ├── few_styling_example.dart │ │ ├── form_field_example.dart │ │ ├── multi_select_example.dart │ │ ├── popup_icon_example.dart │ │ ├── popup_image_example.dart │ │ ├── search_example.dart │ │ ├── simple_example.dart │ │ └── with_separators_example.dart │ ├── pubspec.yaml │ ├── pubspec_overrides.yaml │ └── test │ ├── examples │ ├── few_styling_example_test.dart │ ├── flutter_test_config.dart │ ├── form_field_example_test.dart │ ├── goldens │ │ ├── few_styling_example │ │ │ ├── desktop_closed_menu.png │ │ │ ├── desktop_open_menu.png │ │ │ ├── ipad_pro_closed_menu.png │ │ │ ├── ipad_pro_open_menu.png │ │ │ ├── iphone_14_closed_menu.png │ │ │ ├── iphone_14_open_menu.png │ │ │ ├── iphone_8_closed_menu.png │ │ │ ├── iphone_8_open_menu.png │ │ │ ├── pixel_5_closed_menu.png │ │ │ └── pixel_5_open_menu.png │ │ ├── form_field_example │ │ │ ├── desktop_closed_menu.png │ │ │ ├── desktop_open_menu.png │ │ │ ├── ipad_pro_closed_menu.png │ │ │ ├── ipad_pro_open_menu.png │ │ │ ├── iphone_14_closed_menu.png │ │ │ ├── iphone_14_open_menu.png │ │ │ ├── iphone_8_closed_menu.png │ │ │ ├── iphone_8_open_menu.png │ │ │ ├── pixel_5_closed_menu.png │ │ │ └── pixel_5_open_menu.png │ │ ├── multi_select_example │ │ │ ├── desktop_closed_menu.png │ │ │ ├── desktop_open_menu.png │ │ │ ├── ipad_pro_closed_menu.png │ │ │ ├── ipad_pro_open_menu.png │ │ │ ├── iphone_14_closed_menu.png │ │ │ ├── iphone_14_open_menu.png │ │ │ ├── iphone_8_closed_menu.png │ │ │ ├── iphone_8_open_menu.png │ │ │ ├── pixel_5_closed_menu.png │ │ │ └── pixel_5_open_menu.png │ │ ├── popup_icon_example │ │ │ ├── desktop_closed_menu.png │ │ │ ├── desktop_open_menu.png │ │ │ ├── ipad_pro_closed_menu.png │ │ │ ├── ipad_pro_open_menu.png │ │ │ ├── iphone_14_closed_menu.png │ │ │ ├── iphone_14_open_menu.png │ │ │ ├── iphone_8_closed_menu.png │ │ │ ├── iphone_8_open_menu.png │ │ │ ├── pixel_5_closed_menu.png │ │ │ └── pixel_5_open_menu.png │ │ ├── popup_image_example │ │ │ ├── desktop_closed_menu.png │ │ │ ├── desktop_open_menu.png │ │ │ ├── ipad_pro_closed_menu.png │ │ │ ├── ipad_pro_open_menu.png │ │ │ ├── iphone_14_closed_menu.png │ │ │ ├── iphone_14_open_menu.png │ │ │ ├── iphone_8_closed_menu.png │ │ │ ├── iphone_8_open_menu.png │ │ │ ├── pixel_5_closed_menu.png │ │ │ └── pixel_5_open_menu.png │ │ ├── search_example │ │ │ ├── desktop_closed_menu.png │ │ │ ├── desktop_open_menu.png │ │ │ ├── ipad_pro_closed_menu.png │ │ │ ├── ipad_pro_open_menu.png │ │ │ ├── iphone_14_closed_menu.png │ │ │ ├── iphone_14_open_menu.png │ │ │ ├── iphone_8_closed_menu.png │ │ │ ├── iphone_8_open_menu.png │ │ │ ├── pixel_5_closed_menu.png │ │ │ └── pixel_5_open_menu.png │ │ ├── simple_example │ │ │ ├── desktop_closed_menu.png │ │ │ ├── desktop_open_menu.png │ │ │ ├── ipad_pro_closed_menu.png │ │ │ ├── ipad_pro_open_menu.png │ │ │ ├── iphone_14_closed_menu.png │ │ │ ├── iphone_14_open_menu.png │ │ │ ├── iphone_8_closed_menu.png │ │ │ ├── iphone_8_open_menu.png │ │ │ ├── pixel_5_closed_menu.png │ │ │ └── pixel_5_open_menu.png │ │ └── with_separators_example │ │ │ ├── desktop_closed_menu.png │ │ │ ├── desktop_open_menu.png │ │ │ ├── ipad_pro_closed_menu.png │ │ │ ├── ipad_pro_open_menu.png │ │ │ ├── iphone_14_closed_menu.png │ │ │ ├── iphone_14_open_menu.png │ │ │ ├── iphone_8_closed_menu.png │ │ │ ├── iphone_8_open_menu.png │ │ │ ├── pixel_5_closed_menu.png │ │ │ └── pixel_5_open_menu.png │ ├── multi_select_example_test.dart │ ├── popup_icon_example_test.dart │ ├── popup_image_example_test.dart │ ├── search_example_test.dart │ ├── simple_example_test.dart │ ├── test_app.dart │ └── with_separators_example_test.dart │ └── utils │ ├── run_golden_tests.dart │ ├── test_variants.dart │ ├── utils.dart │ └── widget_tester_utils.dart └── pubspec.yaml /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | 4 | updates: 5 | # GitHub actions 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | interval: weekly 10 | # Workspace root 11 | - package-ecosystem: "pub" 12 | directory: "/" 13 | schedule: 14 | interval: weekly 15 | # Packages 16 | - package-ecosystem: "pub" 17 | directory: "/packages/dropdown_button2" 18 | schedule: 19 | interval: weekly 20 | - package-ecosystem: "pub" 21 | directory: "/packages/dropdown_button2_test" 22 | schedule: 23 | interval: weekly 24 | -------------------------------------------------------------------------------- /.github/images/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedLSayed9/dropdown_button2/6413452f9e5ce58fc993bc7c8453baeeba25c2f4/.github/images/banner.jpg -------------------------------------------------------------------------------- /.github/images/few_styling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedLSayed9/dropdown_button2/6413452f9e5ce58fc993bc7c8453baeeba25c2f4/.github/images/few_styling.png -------------------------------------------------------------------------------- /.github/images/form_field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedLSayed9/dropdown_button2/6413452f9e5ce58fc993bc7c8453baeeba25c2f4/.github/images/form_field.png -------------------------------------------------------------------------------- /.github/images/multi_select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedLSayed9/dropdown_button2/6413452f9e5ce58fc993bc7c8453baeeba25c2f4/.github/images/multi_select.png -------------------------------------------------------------------------------- /.github/images/popup_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedLSayed9/dropdown_button2/6413452f9e5ce58fc993bc7c8453baeeba25c2f4/.github/images/popup_icon.png -------------------------------------------------------------------------------- /.github/images/popup_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedLSayed9/dropdown_button2/6413452f9e5ce58fc993bc7c8453baeeba25c2f4/.github/images/popup_image.png -------------------------------------------------------------------------------- /.github/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedLSayed9/dropdown_button2/6413452f9e5ce58fc993bc7c8453baeeba25c2f4/.github/images/search.png -------------------------------------------------------------------------------- /.github/images/simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedLSayed9/dropdown_button2/6413452f9e5ce58fc993bc7c8453baeeba25c2f4/.github/images/simple.png -------------------------------------------------------------------------------- /.github/images/with_separators.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedLSayed9/dropdown_button2/6413452f9e5ce58fc993bc7c8453baeeba25c2f4/.github/images/with_separators.png -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [master, main] 6 | paths-ignore: 7 | - "**.md" 8 | pull_request: 9 | paths-ignore: 10 | - "**.md" 11 | 12 | jobs: 13 | build: 14 | name: Run flutter test and analyze 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | channel: 19 | - stable 20 | # We'll add master after migrating to Melos 7.x.x and flutter workspaces 21 | # - master 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Setup Flutter environment 27 | uses: subosito/flutter-action@v2.7.1 28 | with: 29 | channel: ${{ matrix.channel }} 30 | 31 | - name: Set environment paths 32 | run: | 33 | echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH 34 | echo "PUB_CACHE="$HOME/.pub-cache"" >> $GITHUB_ENV 35 | 36 | - name: Setup melos 37 | run: | 38 | dart pub global activate melos 39 | melos --version 40 | melos bootstrap 41 | 42 | - name: Verify formatting 43 | # Remove --line-length=100 when upgrading the min dart sdk to at least 3.7 44 | run: melos exec dart format --output=none --set-exit-if-changed --line-length=100 . 45 | 46 | # Consider passing '--fatal-infos' for slightly stricter analysis. 47 | - name: Run flutter analyze 48 | run: melos run analyze 49 | 50 | - name: Run tests 51 | run: melos run test 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | coverage/ 3 | *.class 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | .vscode/settings.json 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | build/ 34 | pubspec.lock 35 | 36 | # Android related 37 | **/android/**/gradle-wrapper.jar 38 | **/android/.gradle 39 | **/android/captures/ 40 | **/android/gradlew 41 | **/android/gradlew.bat 42 | **/android/local.properties 43 | **/android/**/GeneratedPluginRegistrant.java 44 | 45 | # iOS/XCode related 46 | **/ios/**/*.mode1v3 47 | **/ios/**/*.mode2v3 48 | **/ios/**/*.moved-aside 49 | **/ios/**/*.pbxuser 50 | **/ios/**/*.perspectivev3 51 | **/ios/**/*sync/ 52 | **/ios/**/.sconsign.dblite 53 | **/ios/**/.tags* 54 | **/ios/**/.vagrant/ 55 | **/ios/**/DerivedData/ 56 | **/ios/**/Icon? 57 | **/ios/**/Pods/ 58 | **/ios/**/.symlinks/ 59 | **/ios/**/profile 60 | **/ios/**/xcuserdata 61 | **/ios/.generated/ 62 | **/ios/Flutter/App.framework 63 | **/ios/Flutter/Flutter.framework 64 | **/ios/Flutter/Flutter.podspec 65 | **/ios/Flutter/Generated.xcconfig 66 | **/ios/Flutter/ephemeral 67 | **/ios/Flutter/app.flx 68 | **/ios/Flutter/app.zip 69 | **/ios/Flutter/flutter_assets/ 70 | **/ios/Flutter/flutter_export_environment.sh 71 | **/ios/ServiceDefinitions.json 72 | **/ios/Runner/GeneratedPluginRegistrant.* 73 | 74 | # Exceptions to above rules. 75 | !**/ios/**/default.mode1v3 76 | !**/ios/**/default.mode2v3 77 | !**/ios/**/default.pbxuser 78 | !**/ios/**/default.perspectivev3 79 | 80 | # don't check in golden failure output 81 | **/failures/*.png -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Golden", 9 | "request": "launch", 10 | "type": "dart", 11 | "codeLens": { 12 | "for": ["run-test", "run-test-file"] 13 | }, 14 | "args": ["--update-goldens"] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedLSayed9/dropdown_button2/6413452f9e5ce58fc993bc7c8453baeeba25c2f4/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 AHMED ELSAYED 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Lint rules of flutter/packages with some modifications. 2 | # 3 | # This file is a copy of analysis_options.yaml from flutter repo 4 | # as of 2022-07-27, but with some modifications marked with 5 | # "DIFFERENT FROM FLUTTER/FLUTTER" below. The file is expected to 6 | # be kept in sync with the master file from the flutter repo. 7 | 8 | analyzer: 9 | language: 10 | strict-casts: true 11 | strict-raw-types: true 12 | strict-inference: true 13 | errors: 14 | # allow self-reference to deprecated members (we do this because otherwise we have 15 | # to annotate every member in every test, assert, etc, when we deprecate something) 16 | deprecated_member_use_from_same_package: ignore 17 | exclude: # DIFFERENT FROM FLUTTER/FLUTTER 18 | # Ignore generated files 19 | - "**/*.g.dart" 20 | - "**/*.mocks.dart" # Mockito @GenerateMocks 21 | 22 | formatter: 23 | page_width: 100 24 | 25 | linter: 26 | rules: 27 | # This list is derived from the list of all available lints located at 28 | # https://github.com/dart-lang/linter/blob/master/example/all.yaml 29 | - always_declare_return_types 30 | - always_put_control_body_on_new_line 31 | # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 32 | # - always_specify_types # removed for redundancy: https://github.com/dart-lang/linter/issues/1848 33 | # - always_use_package_imports # we do this commonly 34 | - annotate_overrides 35 | # - avoid_annotating_with_dynamic # conflicts with always_specify_types 36 | - avoid_bool_literals_in_conditional_expressions 37 | # - avoid_catches_without_on_clauses # blocked on https://github.com/dart-lang/linter/issues/3023 38 | # - avoid_catching_errors # blocked on https://github.com/dart-lang/linter/issues/3023 39 | # - avoid_classes_with_only_static_members: false # False positive for custom enum-like classes (such as Flutter's "Colors") 40 | - avoid_double_and_int_checks 41 | - avoid_dynamic_calls 42 | - avoid_empty_else 43 | - avoid_equals_and_hash_code_on_mutable_classes 44 | - avoid_escaping_inner_quotes 45 | - avoid_field_initializers_in_const_classes 46 | # - avoid_final_parameters # incompatible with prefer_final_parameters 47 | - avoid_function_literals_in_foreach_calls 48 | - avoid_implementing_value_types 49 | - avoid_init_to_null 50 | - avoid_js_rounded_ints 51 | # - avoid_multiple_declarations_per_line # seems to be a stylistic choice we don't subscribe to 52 | - avoid_null_checks_in_equality_operators 53 | # - avoid_positional_boolean_parameters # would have been nice to enable this but by now there's too many places that break it 54 | - avoid_print 55 | # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) 56 | - avoid_redundant_argument_values 57 | - avoid_relative_lib_imports 58 | - avoid_renaming_method_parameters 59 | - avoid_return_types_on_setters 60 | - avoid_returning_null_for_void 61 | # - avoid_returning_this # there are enough valid reasons to return `this` that this lint ends up with too many false positives 62 | - avoid_setters_without_getters 63 | - avoid_shadowing_type_parameters 64 | - avoid_single_cascade_in_expression_statements 65 | - avoid_slow_async_io 66 | - avoid_type_to_string 67 | - avoid_types_as_parameter_names 68 | # - avoid_types_on_closure_parameters # conflicts with always_specify_types 69 | - avoid_unnecessary_containers 70 | - avoid_unused_constructor_parameters 71 | - avoid_void_async 72 | # - avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere 73 | - await_only_futures 74 | - camel_case_extensions 75 | - camel_case_types 76 | - cancel_subscriptions 77 | # - cascade_invocations # doesn't match the typical style of this repo 78 | - cast_nullable_to_non_nullable 79 | # - close_sinks # not reliable enough 80 | # - combinators_ordering # DIFFERENT FROM FLUTTER/FLUTTER: This isn't available on stable yet. 81 | # - comment_references # blocked on https://github.com/dart-lang/linter/issues/1142 82 | - conditional_uri_does_not_exist 83 | # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 84 | - control_flow_in_finally 85 | - curly_braces_in_flow_control_structures 86 | - depend_on_referenced_packages 87 | - deprecated_consistency 88 | # - diagnostic_describe_all_properties # enabled only at the framework level (packages/flutter/lib) 89 | - directives_ordering 90 | # - discarded_futures # not yet tested 91 | # - do_not_use_environment # there are appropriate times to use the environment, especially in our tests and build logic 92 | - empty_catches 93 | - empty_constructor_bodies 94 | - empty_statements 95 | - eol_at_end_of_file 96 | - exhaustive_cases 97 | - file_names 98 | - flutter_style_todos 99 | - hash_and_equals 100 | - implementation_imports 101 | - collection_methods_unrelated_type 102 | # - join_return_with_assignment # not required by flutter style 103 | - leading_newlines_in_multiline_strings 104 | - library_names 105 | - library_prefixes 106 | - library_private_types_in_public_api 107 | # - lines_longer_than_80_chars # not required by flutter style 108 | # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/linter/issues/453 109 | - missing_whitespace_between_adjacent_strings 110 | - no_adjacent_strings_in_list 111 | - no_default_cases 112 | - no_duplicate_case_values 113 | - no_leading_underscores_for_library_prefixes 114 | - no_leading_underscores_for_local_identifiers 115 | - no_logic_in_create_state 116 | - no_runtimeType_toString # DIFFERENT FROM FLUTTER/FLUTTER 117 | - non_constant_identifier_names 118 | - noop_primitive_operations 119 | - null_check_on_nullable_type_parameter 120 | - null_closures 121 | # - omit_local_variable_types # opposite of always_specify_types 122 | # - one_member_abstracts # too many false positives 123 | - only_throw_errors # this does get disabled in a few places where we have legacy code that uses strings et al 124 | - overridden_fields 125 | - package_names 126 | - package_prefixed_library_names 127 | # - parameter_assignments # we do this commonly 128 | - prefer_adjacent_string_concatenation 129 | - prefer_asserts_in_initializer_lists 130 | # - prefer_asserts_with_message # not required by flutter style 131 | - prefer_collection_literals 132 | - prefer_conditional_assignment 133 | - prefer_const_constructors 134 | - prefer_const_constructors_in_immutables 135 | - prefer_const_declarations 136 | - prefer_const_literals_to_create_immutables 137 | # - prefer_constructors_over_static_methods # far too many false positives 138 | - prefer_contains 139 | # - prefer_double_quotes # opposite of prefer_single_quotes 140 | # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods 141 | - prefer_final_fields 142 | - prefer_final_in_for_each 143 | - prefer_final_locals 144 | # - prefer_final_parameters # we should enable this one day when it can be auto-fixed (https://github.com/dart-lang/linter/issues/3104), see also parameter_assignments 145 | - prefer_for_elements_to_map_fromIterable 146 | - prefer_foreach 147 | - prefer_function_declarations_over_variables 148 | - prefer_generic_function_type_aliases 149 | - prefer_if_elements_to_conditional_expressions 150 | - prefer_if_null_operators 151 | - prefer_initializing_formals 152 | - prefer_inlined_adds 153 | # - prefer_int_literals # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#use-double-literals-for-double-constants 154 | - prefer_interpolation_to_compose_strings 155 | - prefer_is_empty 156 | - prefer_is_not_empty 157 | - prefer_is_not_operator 158 | - prefer_iterable_whereType 159 | # - prefer_mixin # Has false positives, see https://github.com/dart-lang/linter/issues/3018 160 | # - prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere 161 | - prefer_null_aware_operators 162 | - prefer_relative_imports 163 | - prefer_single_quotes 164 | - prefer_spread_collections 165 | - prefer_typing_uninitialized_variables 166 | - prefer_void_to_null 167 | - provide_deprecation_message 168 | - public_member_api_docs # DIFFERENT FROM FLUTTER/FLUTTER 169 | - recursive_getters 170 | # - require_trailing_commas # blocked on https://github.com/dart-lang/sdk/issues/47441 171 | - secure_pubspec_urls 172 | - sized_box_for_whitespace 173 | # - sized_box_shrink_expand # not yet tested 174 | - slash_for_doc_comments 175 | - sort_child_properties_last 176 | - sort_constructors_first 177 | - sort_unnamed_constructors_first 178 | - test_types_in_equals 179 | - throw_in_finally 180 | - tighten_type_of_initializing_formals 181 | - type_annotate_public_apis # subset of always_specify_types 182 | - type_init_formals 183 | - unawaited_futures # DIFFERENT FROM FLUTTER/FLUTTER: It's disabled there for "too many false positives"; that's not an issue here, and missing awaits have caused production issues in plugins. 184 | - unnecessary_await_in_return 185 | - unnecessary_brace_in_string_interps 186 | - unnecessary_const 187 | - unnecessary_constructor_name 188 | # - unnecessary_final # conflicts with prefer_final_locals 189 | - unnecessary_getters_setters 190 | # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 191 | - unnecessary_late 192 | - unnecessary_new 193 | - unnecessary_null_aware_assignments 194 | - unnecessary_null_aware_operator_on_extension_on_nullable 195 | - unnecessary_null_checks 196 | - unnecessary_null_in_if_null_operators 197 | - unnecessary_nullable_for_final_variable_declarations 198 | - unnecessary_overrides 199 | - unnecessary_parenthesis 200 | # - unnecessary_raw_strings # what's "necessary" is a matter of opinion; consistency across strings can help readability more than this lint 201 | - unnecessary_statements 202 | - unnecessary_string_escapes 203 | - unnecessary_string_interpolations 204 | - unnecessary_this 205 | - unnecessary_to_list_in_spreads 206 | - unrelated_type_equality_checks 207 | - use_build_context_synchronously 208 | # - use_colored_box # not yet tested 209 | # - use_decorated_box # not yet tested 210 | # - use_enums # not yet tested 211 | - use_full_hex_values_for_flutter_colors 212 | - use_function_type_syntax_for_parameters 213 | - use_if_null_to_convert_nulls_to_bools 214 | - use_is_even_rather_than_modulo 215 | - use_key_in_widget_constructors 216 | - use_late_for_private_fields_and_variables 217 | - use_named_constants 218 | - use_raw_strings 219 | - use_rethrow_when_possible 220 | - use_setters_to_change_properties 221 | # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 222 | - use_super_parameters 223 | - use_test_throws_matchers 224 | # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review 225 | - valid_regexps 226 | - void_checks 227 | -------------------------------------------------------------------------------- /dart_test.yaml: -------------------------------------------------------------------------------- 1 | tags: 2 | golden: 3 | -------------------------------------------------------------------------------- /melos.yaml: -------------------------------------------------------------------------------- 1 | name: dropdown_button2_workspace 2 | repository: https://github.com/AhmedLSayed9/dropdown_button2 3 | 4 | packages: 5 | - packages/** 6 | - examples/** 7 | 8 | command: 9 | version: 10 | message: | 11 | Release 12 | 13 | {new_package_versions} 14 | bootstrap: 15 | environment: 16 | sdk: ">=3.4.0 <4.0.0" 17 | flutter: ">=3.22.0" 18 | 19 | scripts: 20 | lint:all: 21 | run: melos run analyze && melos run format 22 | description: Run all static analysis checks. 23 | 24 | analyze: 25 | # We are setting the concurrency to 1 because a higher concurrency can crash 26 | # the analysis server on low performance machines (like GitHub Actions). 27 | run: | 28 | melos exec -c 1 -- \ 29 | flutter analyze --fatal-infos 30 | description: Run `flutter analyze` for all packages. 31 | 32 | format: 33 | # Remove --line-length=100 when upgrading the min dart sdk to at least 3.7 34 | run: melos exec dart format --line-length=100 . 35 | description: Run `dart format` for all packages. 36 | 37 | test:select: 38 | run: melos exec -- flutter test 39 | packageFilters: 40 | dirExists: test 41 | description: Run `flutter test` for selected packages. 42 | 43 | test: 44 | run: melos run test:select --no-select 45 | description: Run all Flutter tests in this project. 46 | 47 | coverage: 48 | run: | 49 | melos exec -- flutter test --coverage && 50 | melos exec -- genhtml coverage/lcov.info --output-directory=coverage/ 51 | packageFilters: 52 | dirExists: test 53 | description: Generate coverage for the selected package. 54 | 55 | update-goldens: 56 | run: melos exec -- flutter test --update-goldens 57 | packageFilters: 58 | dirExists: test 59 | description: Re-generate all golden test files 60 | -------------------------------------------------------------------------------- /packages/dropdown_button2/.pubignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | coverage/ 3 | *.class 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Flutter/Dart/Pub related 20 | **/doc/api/ 21 | .dart_tool/ 22 | .flutter-plugins 23 | .flutter-plugins-dependencies 24 | .packages 25 | .pub-cache/ 26 | .pub/ 27 | build/ 28 | pubspec.lock -------------------------------------------------------------------------------- /packages/dropdown_button2/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## UNRELEASED 2 | 3 | - Avoid dropdown internal FocusNode listener leak when replaced by an external FocusNode 4 | 5 | ## 3.0.0-beta.22 6 | 7 | - Fix errorStyle has no effect for DropdownButtonFormField2, closes #327. 8 | - DropdownRoutePage should dispose the created ScrollController [Flutter core]. 9 | - Remove 'must be non-null' and 'must not be null' comments [Flutter core]. 10 | - Form fields onChange callback should be called on reset [Flutter core]. 11 | - Implement switch expressions. 12 | - Fix memory leak in CurvedAnimation [Flutter core]. 13 | - Avoid Container objects when possible for better performance [Flutter core]. 14 | - Add semantics to dropdown menu items [Flutter core]. 15 | - Support helperStyle/helperMaxLines/errorMaxLines for DropdownButtonFormField2. 16 | - Add `MenuItemStyleData.useDecorationHorizontalPadding`, used to determine whether to use the horizontal padding from "decoration.contentPadding" for menu items when using `DropdownButtonFormField2`. 17 | - Use decoration hint text as the default value for dropdown button hints [Flutter core]. 18 | - Update SDK constraints: ">=3.4.0 <4.0.0" 19 | - Fix DropdownButtonFormField clips text when large text scale is used [Flutter core]. 20 | - Fix DropdownButtonFormField padding when ButtonTheme.alignedDropdown is true [Flutter core]. 21 | - Add barrierCoversButton to DropdownButtonFormField2. 22 | - Respect button's borderRadius when barrierCoversButton is false. 23 | - Respect inputDecoration's borderRadius when barrierCoversButton is false. 24 | - Support BorderRadiusDirectional for dropdown menu. 25 | - Fix barrier when using TextDirection.rtl while barrierCoversButton set to false. 26 | - Take InputDecoration's densityOffset into account when determining the button size. 27 | 28 | ## 3.0.0-beta.21 29 | 30 | - Fix menu limits when using searchable dropdown with separators, closes #214. 31 | 32 | ## 3.0.0-beta.20 33 | 34 | - Remove an assert from updateSelectedIndex method. 35 | - Update examples. 36 | 37 | ## 3.0.0-beta.19 38 | 39 | - Enhance the display of error/helper elements at DropdownButtonFormField2, closes #199. 40 | 41 | ## 3.0.0-beta.18 42 | 43 | - Replaces textScaleFactor with TextScaler [Flutter core]. 44 | - Fix DropdownButtonFormField2 ink response radius for different input borders. 45 | - Fix error border not showing for DropdownButtonFormField2, closes #297 & #319. 46 | 47 | ## 3.0.0-beta.17 48 | 49 | - Enhance scroll position when using searchable dropdown, closes #285. 50 | - Temporarily fix ink splash gets displayed over search widget, closes #290. 51 | - Add copyWith method for style data classes, closes #314. 52 | 53 | ## 3.0.0-beta.16 54 | 55 | - Fix dropdown menu position when window changes horizontally, closes #243. 56 | 57 | ## 3.0.0-beta.15 58 | 59 | - Always call `onChanged` when tapping enabled item, closes #275. 60 | 61 | ## 3.0.0-beta.14 62 | 63 | - Optimize scroll performance when dealing with large items list. 64 | - Update SDK constraints: ">=3.2.0 <4.0.0" 65 | 66 | ## 3.0.0-beta.13 67 | 68 | - **BREAKING**: Add `openDropdownListenable` property that can be used to programmatically open the dropdown menu. 69 | 70 | Instead of: 71 | 72 | ```dart 73 | final dropdownKey = GlobalKey(); 74 | 75 | @override 76 | Widget build(BuildContext context) { 77 | return Column( 78 | children:[ 79 | DropdownButton2( 80 | // Other properties... 81 | key: dropdownKey, 82 | ); 83 | // Open the dropdown programmatically, like when another button is pressed: 84 | ElevatedButton( 85 | onTap: () => dropdownKey.currentState!.callTap(), 86 | ), 87 | ], 88 | ); 89 | } 90 | ``` 91 | 92 | do: 93 | 94 | ```dart 95 | final openDropdownListenable = ValueNotifier(null); 96 | 97 | @override 98 | Widget build(BuildContext context) { 99 | return Column( 100 | children:[ 101 | DropdownButton2( 102 | // Other properties... 103 | openDropdownListenable: openDropdownListenable, 104 | ); 105 | // Open the dropdown programmatically, like when another button is pressed: 106 | ElevatedButton( 107 | onTap: () => openDropdownListenable.value = Object(), 108 | ), 109 | ], 110 | ); 111 | } 112 | ``` 113 | 114 | ## 3.0.0-beta.12 115 | 116 | - Add `dropdownBuilder` property for DropdownStyleData, it can be used to customize the dropdown menu. 117 | 118 | ## 3.0.0-beta.11 119 | 120 | - Introduce `valueListenable` and `multiValueListenable`, which replaces SetState with ValueListenable. 121 | - Support implementing select all option (Check multi-select example), closes #121 and #167. 122 | 123 | ## 3.0.0-beta.10 124 | 125 | - Add the possibility to display a dropdown menu centered through `DropdownDirection.center`. 126 | - Add `barrierCoversButton` property, used to specify whether the modal barrier should cover the dropdown button or not. 127 | 128 | ## 3.0.0-beta.9 129 | 130 | - Update DropdownItem to respect intrinsicHeight. 131 | - Update added menu padding to include button icon. 132 | 133 | ## 3.0.0-beta.8 134 | 135 | - Add `foregroundDecoration` property for ButtonStyleData. 136 | 137 | ## 3.0.0-beta.7 138 | 139 | - Add intrinsicHeight property to DropdownItem. This enables setting item's height according to its intrinsic height. 140 | 141 | ## 3.0.0-beta.6 142 | 143 | - Fix isExpanded and alignment functionality. 144 | 145 | ## 3.0.0-beta.5 146 | 147 | - Enhance rendering performance when dealing with big items list. 148 | 149 | ## 3.0.0-beta.4 150 | 151 | - Add `noResultsWidget` property for DropdownSearchData. It can be used to show some widget when the search results are empty. 152 | - Rename searchInnerWidget[Height] to searchBarWidget[Height]. 153 | 154 | ## 3.0.0-beta.3 155 | 156 | - Fix inkwell covers error message. 157 | 158 | ## 3.0.0-beta.2 159 | 160 | - Add border radius parameter for menu item. 161 | 162 | ## 3.0.0-beta.1 163 | 164 | - Fix formatting. 165 | 166 | ## 3.0.0-beta.0 167 | 168 | - **BREAKING**: Replaces DropdownMenuItem with DropdownItem to provide extra functionality. 169 | 170 | Instead of: 171 | 172 | ```dart 173 | items: items.map((String item) => DropdownMenuItem(...)).toList(), 174 | ``` 175 | 176 | do: 177 | 178 | ```dart 179 | items: items.map((String item) => DropdownItem(...)).toList(), 180 | ``` 181 | 182 | - Add `closeOnTap` property to DropdownItem. It controls whether the dropdown should close when the item is tapped. 183 | 184 | - **BREAKING**: Support setting different heights for items. 185 | 186 | Instead of: 187 | 188 | ```dart 189 | items: items 190 | .map((String item) => DropdownItem( 191 | value: item, 192 | child: Text(item), 193 | )) 194 | .toList(), 195 | menuItemStyleData: const MenuItemStyleData( 196 | height: 40, 197 | ), 198 | ``` 199 | 200 | do: 201 | 202 | ```dart 203 | items: items 204 | .map((String item) => DropdownItem( 205 | value: item, 206 | height: 40, 207 | child: Text(item), 208 | )) 209 | .toList(), 210 | ``` 211 | 212 | - Support adding separator widget internally, closes #134. 213 | 214 | - Update highlight behavior, closes #184. 215 | 216 | ## 2.3.9 217 | 218 | - Use melos to separate package golden tests, closes #176. 219 | 220 | ## 2.3.8 221 | 222 | - Use null-aware operators for thumbVisibility and thickness. 223 | 224 | ## 2.3.7 225 | 226 | - Remove deprecated isAlwaysShown member. 227 | 228 | ## 2.3.6 229 | 230 | - Remove unnecessary packages from `dependencies`. 231 | 232 | ## 2.3.5 233 | 234 | - Fix ScrollBar theming. 235 | 236 | ## 2.3.4 237 | 238 | - `isFullScreen` should be null by default, fixes #157. 239 | 240 | ## 2.3.3 241 | 242 | - Support ScrollBar theming for Cupertino, closes #154. 243 | 244 | ## 2.3.1 245 | 246 | - Update gitignore/pubignore, fixes #153. 247 | 248 | ## 2.2.2 249 | 250 | - Ignore goldens and assets for pub.dev (reduces package size) 251 | 252 | ## 2.2.1 253 | 254 | - Fix README images. 255 | 256 | ## 2.2.0 257 | 258 | - Update DropdownButtonFormField2 decoration behavior. (#152) 259 | - Squashed MediaQuery InheritedModel [Flutter core]. 260 | - Fix rebuild issue of openMenuIcon. 261 | - Fix ink response radius for DropdownButtonFormField2. 262 | - Update README and examples. 263 | 264 | ## 2.1.4 265 | 266 | - Fix button falsely maintaining focus, closes #152. 267 | 268 | ## 2.1.3 269 | 270 | - Fix assertion failure when dropdownStyleData's decoration has an image. 271 | - Fix IgnorePointer to only block user interactions (Flutter Dropdown Update). 272 | 273 | ## 2.1.2 274 | 275 | - Revert "Remove the deprecated window" and ignore instead. 276 | 277 | ## 2.1.1 278 | 279 | - Remove the deprecated `window`. 280 | 281 | ## 2.1.0 282 | 283 | - Update dropdown menu's vertical extent. 284 | 285 | ## 2.0.1 286 | 287 | - `DropdownStyleData.isFullScreen` is deprecated in favor of `DropdownStyleData.useRootNavigator`. 288 | - Add `DropdownStyleData.useSafeArea`, used to determine if the dropdown menu should only display in 289 | safe areas of the screen. 290 | 291 | ## 2.0.0 292 | 293 | ### Breaking Changes: 294 | 295 | - Refactor & organize related parameters to sub-classes (check Options at README). 296 | - Prefer overlayColor for button/menuItem's ink response (supports desktop/web inkwell behavior 297 | customization). 298 | - Add scrollbarTheme parameter, used to configures scrollbar's theme (#113). 299 | - Add openInterval parameter, used to configure menu animation speed (#128). 300 | - Add selectedMenuItemBuilder parameter, used to customize the selected menu item (#124). 301 | - Improve SearchMatchFn's type inference. 302 | - Replace deprecated subtitle1 by titleMedium. 303 | - Update README. 304 | 305 | ## 1.9.4 306 | 307 | - Use generics with searchMatchFn for item type inference. 308 | 309 | ## 1.9.3 310 | 311 | - Add searchBarWidgetHeight, it fixes menu limits and scroll offset when using searchBarWidget. 312 | 313 | ## 1.9.2 314 | 315 | - Avoid overlapping menu items with keyboard when using searchable dropdown. 316 | 317 | ## 1.9.1 318 | 319 | - Add dropdownButtonKey parameter to DropdownButtonFormField2, it allows accessing 320 | DropdownButton2State. 321 | - Update README. 322 | 323 | ## 1.9.0 324 | 325 | - Adapt to max items width when buttonWidth & dropdownWidth is null. 326 | - Prevent dropdownWidth to exceed max screen width. 327 | - Add dropdownDirection parameter, it controls the direction of the dropdown menu in relation to the 328 | button. 329 | - Update README. 330 | 331 | ## 1.8.9 332 | 333 | - Add buttonOverlayColor parameter, It changes the overlay color of the button's InkWell. 334 | - Update README. 335 | 336 | ## 1.8.8 337 | 338 | - Update README. 339 | 340 | ## 1.8.7 341 | 342 | - Add buttonSplashColor parameter, It changes the splash color of the button's InkWell. close #79, 343 | #89. 344 | - Add buttonHighlightColor parameter, It changes the highlight color of the button's InkWell. close 345 | #79, #89. 346 | 347 | ## 1.8.6 348 | 349 | - Add itemSplashColor parameter, It changes the splash color of the item's InkWell. close #79, #89. 350 | - Add itemHighlightColor parameter, It changes the highlight color of the item's InkWell. close #79, 351 | #89. 352 | 353 | ## 1.8.5 354 | 355 | - Fix typo in clampDouble method call, fixes #88. 356 | 357 | ## 1.8.4 358 | 359 | - Fix Dropdown menu dx offset range, fixes #86. 360 | 361 | ## 1.8.3 362 | 363 | - Fix the ability to increase dy offset for some cases, fixes #85. 364 | 365 | ## 1.8.2 366 | 367 | - define clampDouble within DropdownButton2 library. 368 | 369 | ## 1.8.1 370 | 371 | - Add dropdownScrollPadding parameter, it add padding to the dropdown menu including the scrollbar. 372 | - Update README. 373 | 374 | ## 1.8.0 375 | 376 | ### Breaking Changes: 377 | 378 | - Remove customItemsIndexes and customItemsHeight parameters. 379 | - Add customItemsHeights parameter, it uses different predefined heights for the menu items. close 380 | #71. 381 | - Switched to a double variant of clamp to avoid boxing (Flutter Dropdown Update). 382 | - Replace empty Container with const SizedBox (Flutter Dropdown Update). 383 | - Update README. 384 | 385 | ## 1.7.2 386 | 387 | - Fix DropdownButtonFormField ripple effect offset to top by 1px, fixes #65. 388 | 389 | ## 1.7.1 390 | 391 | - Fix DropdownButtonFormField InkWell spreads to error message, fixes #56. 392 | - Prevent Selected item from rendering before rest of the list items, fixes #57. 393 | 394 | ## 1.7.0 395 | 396 | - Update DropdownButton menu clip (Flutter Dropdown Update). 397 | - Fix hint alignment when selectedItemBuilder is non-null (Flutter Dropdown Update). 398 | - Modify calculation of dense button height when text scale is large (Flutter Dropdown Update). 399 | - Updating PrimaryScrollController for Desktop, fixes #49. 400 | - Fix DropdownButton inkwell border radius. fixes #53, fixes #54. 401 | 402 | ## 1.6.3 403 | 404 | - Fix openWithLongPress functionality. close #46. 405 | 406 | ## 1.6.2 407 | 408 | - Use buttonDecoration's boxShadow value (if exists) for button's decoration, otherwise use 409 | buttonElevation. 410 | 411 | ## 1.6.1 412 | 413 | - Fix #39. 414 | - Update README. 415 | 416 | ## 1.6.0 417 | 418 | ### Breaking Changes: 419 | 420 | - Add searching feature: 421 | 422 | * searchController parameter, The TextEditingController used for searchable dropdowns. If null, then 423 | it'll perform as a normal dropdown without searching feature. 424 | * searchBarWidget parameter, The widget to be shown at the top of the dropdown menu for searchable 425 | dropdowns, such as search bar. 426 | * searchMatchFn parameter, The match function used for searchable dropdowns, if null \_ 427 | defaultSearchMatchFn will be used. 428 | 429 | - Improve selectedItemOffset to get accurate scrollOffset when dropdown padding is set. 430 | - Update README. 431 | 432 | ## 1.5.3 433 | 434 | - Add barrierDismissible parameter, you can prevent dismissing the menu by tapping the modal 435 | barrier. 436 | - Add barrierColor parameter, you can change the color of the modal barrier (default is transparent) 437 | . close #35. 438 | - Add barrierLabel parameter, you can set the semantic label used for a dismissible barrier. 439 | - Update README. 440 | 441 | ## 1.5.2 442 | 443 | - Allow opening the button programmatically using GlobalKey. close #33. 444 | 445 | ## 1.5.1 446 | 447 | - Use PlatformDispatcher.instance over window. 448 | - Use super parameters lint. 449 | - Add Multiselect Dropdown with Checkboxes Example. 450 | - Update README. 451 | 452 | ## 1.5.0 453 | 454 | ### Breaking Changes: 455 | 456 | - Flutter 3.0.0 upgrade. 457 | - Update README. 458 | 459 | ## 1.4.0 460 | 461 | ### Breaking Changes: 462 | 463 | - Remove onTap parameter. 464 | - Remove onMenuClose parameter. 465 | - Add onMenuStateChange parameter, It's called when the dropdown menu is opened or closed. close 466 | #24. 467 | - Update README. 468 | 469 | ## 1.3.0 470 | 471 | - Prevent scrollbar and ripple effect from going beyond the menu rounded border boundaries when 472 | scrolling. fix #21. 473 | 474 | ## 1.2.5 475 | 476 | - Update docs & README. 477 | 478 | ## 1.2.4 479 | 480 | - Update docs & README. 481 | 482 | ## 1.2.3 483 | 484 | - Add dropdownFullScreen parameter, if true, menu will open in fullscreen mode (Above AppBar & 485 | TabBar). close #20. 486 | - Update README. 487 | 488 | ## 1.2.2 489 | 490 | - Add selectedItemHighlightColor parameter, It specifies highlight color of the current selected 491 | item. 492 | - Update README. 493 | 494 | ## 1.2.1 495 | 496 | - Add onMenuClose parameter, It calls a function when the dropdown menu is closed. 497 | - Update README. 498 | 499 | ## 1.2.0 500 | 501 | ### Breaking Changes: 502 | 503 | - Fix tappable area for DropdownButtonFormField & add InkWell to 504 | DropdownButton [Flutter Dropdown Update]. 505 | - Inline casts on Element.widget getter to improve web performance [Flutter Dropdown Update]. 506 | - Fix DropdownButtonFormField loses highlight when menu opens and stays highlighted after menu 507 | closes. 508 | - Add iconOnClick parameter, It toggles different icon when dropdown menu open, close #12. 509 | - Update README. 510 | 511 | ## 1.1.1 512 | 513 | - Prevent first item to be highlighted when there's no item selected on web and desktop mode (when 514 | FocusHighlightMode is set to traditional). 515 | - Prevent button's color to change to focusColor when selecting items on web and desktop mode (when 516 | FocusHighlightMode is set to traditional). 517 | 518 | ## 1.1.0 519 | 520 | ### Breaking Changes: 521 | 522 | - Rename itemWidth to dropdownWidth for clearness. 523 | - Prevent items from going beyond the menu rounded border boundaries when scrolling. 524 | - Remove borderRadius from first and last item of the dropdown menu. 525 | - Change List.from to List.of "Dart lint". 526 | - Update README. 527 | 528 | ## 1.0.7 529 | 530 | - BoxShadow can now be added to dropdownDecoration, and if so, it will be used instead of 531 | dropdownElevation. 532 | 533 | ## 1.0.6 534 | 535 | - Update README 536 | 537 | ## 1.0.5 538 | 539 | - Change some parameters names to be more clear. 540 | - Add dropdown decoration as BoxDecoration parameter. 541 | - Add Options table to README. 542 | - Add "How to use DropdownButton2 with dividers" to README Examples. 543 | 544 | ## 1.0.2 545 | 546 | - Add "How to use DropdownButtonFormField2 with Form" to README Examples. 547 | 548 | ## 1.0.1 549 | 550 | - Update README 551 | 552 | ## 1.0.0 553 | 554 | - initRelease 555 | -------------------------------------------------------------------------------- /packages/dropdown_button2/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /packages/dropdown_button2/README.md: -------------------------------------------------------------------------------- 1 | ../../README.md -------------------------------------------------------------------------------- /packages/dropdown_button2/example/custom_dropdown_button2.dart: -------------------------------------------------------------------------------- 1 | import 'package:dropdown_button2/dropdown_button2.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class CustomDropdownButton2 extends StatelessWidget { 6 | const CustomDropdownButton2({ 7 | required this.hint, 8 | required this.valueListenable, 9 | required this.dropdownItems, 10 | required this.onChanged, 11 | this.selectedItemBuilder, 12 | this.hintAlignment, 13 | this.valueAlignment, 14 | this.buttonHeight, 15 | this.buttonWidth, 16 | this.buttonPadding, 17 | this.buttonDecoration, 18 | this.buttonElevation, 19 | this.icon, 20 | this.iconSize, 21 | this.iconEnabledColor, 22 | this.iconDisabledColor, 23 | this.itemHeight, 24 | this.itemPadding, 25 | this.dropdownHeight, 26 | this.dropdownWidth, 27 | this.dropdownPadding, 28 | this.dropdownDecoration, 29 | this.dropdownElevation, 30 | this.scrollbarRadius, 31 | this.scrollbarThickness, 32 | this.scrollbarAlwaysShow, 33 | this.offset = Offset.zero, 34 | super.key, 35 | }); 36 | final String hint; 37 | final ValueListenable? valueListenable; 38 | final List dropdownItems; 39 | final ValueChanged? onChanged; 40 | final DropdownButtonBuilder? selectedItemBuilder; 41 | final Alignment? hintAlignment; 42 | final Alignment? valueAlignment; 43 | final double? buttonHeight, buttonWidth; 44 | final EdgeInsetsGeometry? buttonPadding; 45 | final BoxDecoration? buttonDecoration; 46 | final int? buttonElevation; 47 | final Widget? icon; 48 | final double? iconSize; 49 | final Color? iconEnabledColor; 50 | final Color? iconDisabledColor; 51 | final double? itemHeight; 52 | final EdgeInsetsGeometry? itemPadding; 53 | final double? dropdownHeight, dropdownWidth; 54 | final EdgeInsetsGeometry? dropdownPadding; 55 | final BoxDecoration? dropdownDecoration; 56 | final int? dropdownElevation; 57 | final Radius? scrollbarRadius; 58 | final double? scrollbarThickness; 59 | final bool? scrollbarAlwaysShow; 60 | final Offset offset; 61 | 62 | @override 63 | Widget build(BuildContext context) { 64 | return DropdownButtonHideUnderline( 65 | child: DropdownButton2( 66 | //To avoid long text overflowing. 67 | isExpanded: true, 68 | hint: Container( 69 | alignment: hintAlignment, 70 | child: Text( 71 | hint, 72 | overflow: TextOverflow.ellipsis, 73 | maxLines: 1, 74 | style: TextStyle( 75 | fontSize: 14, 76 | color: Theme.of(context).hintColor, 77 | ), 78 | ), 79 | ), 80 | valueListenable: valueListenable, 81 | items: dropdownItems 82 | .map((String item) => DropdownItem( 83 | value: item, 84 | height: itemHeight ?? 40, 85 | child: Container( 86 | alignment: valueAlignment, 87 | child: Text( 88 | item, 89 | overflow: TextOverflow.ellipsis, 90 | maxLines: 1, 91 | style: const TextStyle( 92 | fontSize: 14, 93 | ), 94 | ), 95 | ), 96 | )) 97 | .toList(), 98 | onChanged: onChanged, 99 | selectedItemBuilder: selectedItemBuilder, 100 | buttonStyleData: ButtonStyleData( 101 | height: buttonHeight ?? 40, 102 | width: buttonWidth ?? 140, 103 | padding: buttonPadding ?? const EdgeInsets.only(left: 14, right: 14), 104 | decoration: buttonDecoration ?? 105 | BoxDecoration( 106 | borderRadius: BorderRadius.circular(14), 107 | border: Border.all( 108 | color: Colors.black45, 109 | ), 110 | ), 111 | elevation: buttonElevation, 112 | ), 113 | iconStyleData: IconStyleData( 114 | icon: icon ?? const Icon(Icons.arrow_forward_ios_outlined), 115 | iconSize: iconSize ?? 12, 116 | iconEnabledColor: iconEnabledColor, 117 | iconDisabledColor: iconDisabledColor, 118 | ), 119 | dropdownStyleData: DropdownStyleData( 120 | //Max height for the dropdown menu & becoming scrollable if there are more items. If you pass Null it will take max height possible for the items. 121 | maxHeight: dropdownHeight ?? 200, 122 | width: dropdownWidth ?? 140, 123 | padding: dropdownPadding, 124 | decoration: dropdownDecoration ?? 125 | BoxDecoration( 126 | borderRadius: BorderRadius.circular(14), 127 | ), 128 | elevation: dropdownElevation ?? 8, 129 | //Null or Offset(0, 0) will open just under the button. You can edit as you want. 130 | offset: offset, 131 | scrollbarTheme: ScrollbarThemeData( 132 | radius: scrollbarRadius ?? const Radius.circular(40), 133 | thickness: scrollbarThickness != null 134 | ? WidgetStateProperty.all(scrollbarThickness!) 135 | : null, 136 | thumbVisibility: scrollbarAlwaysShow != null 137 | ? WidgetStateProperty.all(scrollbarAlwaysShow!) 138 | : null, 139 | ), 140 | ), 141 | menuItemStyleData: MenuItemStyleData( 142 | padding: itemPadding ?? const EdgeInsets.only(left: 14, right: 14), 143 | ), 144 | ), 145 | ); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /packages/dropdown_button2/example/example.dart: -------------------------------------------------------------------------------- 1 | import 'package:dropdown_button2/dropdown_button2.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | void main() { 5 | runApp(const MyApp()); 6 | } 7 | 8 | class MyApp extends StatelessWidget { 9 | const MyApp({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return const MaterialApp( 14 | title: 'DropdownButton2 Demo', 15 | home: MyHomePage(), 16 | ); 17 | } 18 | } 19 | 20 | class MyHomePage extends StatefulWidget { 21 | const MyHomePage({super.key}); 22 | 23 | @override 24 | State createState() => _MyHomePageState(); 25 | } 26 | 27 | class _MyHomePageState extends State { 28 | final List items = [ 29 | 'Item1', 30 | 'Item2', 31 | 'Item3', 32 | 'Item4', 33 | 'Item5', 34 | 'Item6', 35 | 'Item7', 36 | 'Item8', 37 | ]; 38 | final valueListenable = ValueNotifier(null); 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return Scaffold( 43 | body: Center( 44 | child: DropdownButtonHideUnderline( 45 | child: DropdownButton2( 46 | isExpanded: true, 47 | hint: const Row( 48 | children: [ 49 | Icon( 50 | Icons.list, 51 | size: 16, 52 | color: Colors.yellow, 53 | ), 54 | SizedBox( 55 | width: 4, 56 | ), 57 | Expanded( 58 | child: Text( 59 | 'Select Item', 60 | style: TextStyle( 61 | fontSize: 14, 62 | fontWeight: FontWeight.bold, 63 | color: Colors.yellow, 64 | ), 65 | overflow: TextOverflow.ellipsis, 66 | ), 67 | ), 68 | ], 69 | ), 70 | items: items 71 | .map((String item) => DropdownItem( 72 | value: item, 73 | height: 40, 74 | child: Text( 75 | item, 76 | style: const TextStyle( 77 | fontSize: 14, 78 | fontWeight: FontWeight.bold, 79 | color: Colors.white, 80 | ), 81 | overflow: TextOverflow.ellipsis, 82 | ), 83 | )) 84 | .toList(), 85 | valueListenable: valueListenable, 86 | onChanged: (value) { 87 | valueListenable.value = value; 88 | }, 89 | buttonStyleData: ButtonStyleData( 90 | height: 50, 91 | width: 160, 92 | padding: const EdgeInsets.only(left: 14, right: 14), 93 | decoration: BoxDecoration( 94 | borderRadius: BorderRadius.circular(14), 95 | border: Border.all( 96 | color: Colors.black26, 97 | ), 98 | color: Colors.redAccent, 99 | ), 100 | elevation: 2, 101 | ), 102 | iconStyleData: const IconStyleData( 103 | icon: Icon( 104 | Icons.arrow_forward_ios_outlined, 105 | ), 106 | iconSize: 14, 107 | iconEnabledColor: Colors.yellow, 108 | iconDisabledColor: Colors.grey, 109 | ), 110 | dropdownStyleData: DropdownStyleData( 111 | maxHeight: 200, 112 | width: 200, 113 | decoration: BoxDecoration( 114 | borderRadius: BorderRadius.circular(14), 115 | color: Colors.redAccent, 116 | ), 117 | offset: const Offset(-20, 0), 118 | scrollbarTheme: ScrollbarThemeData( 119 | radius: const Radius.circular(40), 120 | thickness: WidgetStateProperty.all(6), 121 | thumbVisibility: WidgetStateProperty.all(true), 122 | ), 123 | ), 124 | menuItemStyleData: const MenuItemStyleData( 125 | padding: EdgeInsets.only(left: 14, right: 14), 126 | ), 127 | ), 128 | ), 129 | ), 130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /packages/dropdown_button2/lib/dropdown_button2.dart: -------------------------------------------------------------------------------- 1 | library dropdown_button2; 2 | 3 | export 'src/dropdown_button2.dart'; 4 | -------------------------------------------------------------------------------- /packages/dropdown_button2/lib/src/button_style_data.dart: -------------------------------------------------------------------------------- 1 | part of 'dropdown_button2.dart'; 2 | 3 | class _ButtonStyleDataBase { 4 | const _ButtonStyleDataBase({ 5 | required this.height, 6 | required this.width, 7 | required this.padding, 8 | required this.decoration, 9 | required this.foregroundDecoration, 10 | required this.elevation, 11 | }); 12 | 13 | /// The height of the button 14 | final double? height; 15 | 16 | /// The width of the button 17 | final double? width; 18 | 19 | /// The inner padding of the Button 20 | final EdgeInsetsGeometry? padding; 21 | 22 | /// The decoration of the Button 23 | final BoxDecoration? decoration; 24 | 25 | /// The decoration to paint in front of the Button 26 | final BoxDecoration? foregroundDecoration; 27 | 28 | /// The elevation of the Button 29 | final int? elevation; 30 | } 31 | 32 | /// A class to configure the theme of the button. 33 | class ButtonStyleData extends _ButtonStyleDataBase { 34 | /// Creates a ButtonStyleData. 35 | /// It's a class to configure the theme of the button. 36 | const ButtonStyleData({ 37 | super.height, 38 | super.width, 39 | super.padding, 40 | super.decoration, 41 | super.foregroundDecoration, 42 | super.elevation, 43 | this.overlayColor, 44 | }); 45 | 46 | /// Defines the ink response focus, hover, and splash colors. 47 | /// 48 | /// This default null property can be used as an alternative to 49 | /// [focusColor], [hoverColor], [highlightColor], and 50 | /// [splashColor]. If non-null, it is resolved against one of 51 | /// [WidgetState.focused], [WidgetState.hovered], and 52 | /// [WidgetState.pressed]. It's convenient to use when the parent 53 | /// widget can pass along its own WidgetStateProperty value for 54 | /// the overlay color. 55 | /// 56 | /// [WidgetState.pressed] triggers a ripple (an ink splash), per 57 | /// the current Material Design spec. The [overlayColor] doesn't map 58 | /// a state to [highlightColor] because a separate highlight is not 59 | /// used by the current design guidelines. See 60 | /// https://material.io/design/interaction/states.html#pressed 61 | /// 62 | /// If the overlay color is null or resolves to null, then [focusColor], 63 | /// [hoverColor], [splashColor] and their defaults are used instead. 64 | /// 65 | /// See also: 66 | /// 67 | /// * The Material Design specification for overlay colors and how they 68 | /// match a component's state: 69 | /// . 70 | final WidgetStateProperty? overlayColor; 71 | 72 | /// Create a clone of the current [ButtonStyleData] but with the provided 73 | /// parameters overridden. 74 | ButtonStyleData copyWith({ 75 | double? height, 76 | double? width, 77 | EdgeInsetsGeometry? padding, 78 | BoxDecoration? decoration, 79 | BoxDecoration? foregroundDecoration, 80 | int? elevation, 81 | WidgetStateProperty? overlayColor, 82 | }) { 83 | return ButtonStyleData( 84 | height: height ?? this.height, 85 | width: width ?? this.width, 86 | padding: padding ?? this.padding, 87 | decoration: decoration ?? this.decoration, 88 | foregroundDecoration: foregroundDecoration ?? this.foregroundDecoration, 89 | elevation: elevation ?? this.elevation, 90 | overlayColor: overlayColor ?? this.overlayColor, 91 | ); 92 | } 93 | } 94 | 95 | /// A class to configure the theme of the button when using DropdownButtonFormField2. 96 | /// 97 | /// Note: To configure the button's overlay colors when using DropdownButtonFormField2, 98 | /// use [InputDecoration] with `filled: true`. This works similarly to TextField. 99 | /// 100 | /// i.e: 101 | /// ```dart 102 | /// decoration: InputDecoration( 103 | /// filled: true, 104 | /// fillColor: Colors.green, 105 | /// hoverColor: Colors.red, 106 | /// focusColor: Colors.blue, 107 | /// ), 108 | /// // Or 109 | /// decoration: InputDecoration( 110 | /// filled: true, 111 | /// fillColor: WidgetStateColor.fromMap({ 112 | /// WidgetState.hovered: Colors.red, 113 | /// WidgetState.focused: Colors.blue, 114 | /// WidgetState.any: Colors.green, 115 | /// }), 116 | /// ), 117 | /// ``` 118 | class FormFieldButtonStyleData extends _ButtonStyleDataBase { 119 | /// Creates a FormFieldButtonStyleData. 120 | /// It's a class to configure the theme of the button when using DropdownButtonFormField2. 121 | const FormFieldButtonStyleData({ 122 | super.height, 123 | super.width, 124 | super.padding, 125 | super.decoration, 126 | super.foregroundDecoration, 127 | super.elevation, 128 | }); 129 | 130 | /// Create a clone of the current [FormFieldButtonStyleData] but with the provided 131 | /// parameters overridden. 132 | FormFieldButtonStyleData copyWith({ 133 | double? height, 134 | double? width, 135 | EdgeInsetsGeometry? padding, 136 | BoxDecoration? decoration, 137 | BoxDecoration? foregroundDecoration, 138 | int? elevation, 139 | }) { 140 | return FormFieldButtonStyleData( 141 | height: height ?? this.height, 142 | width: width ?? this.width, 143 | padding: padding ?? this.padding, 144 | decoration: decoration ?? this.decoration, 145 | foregroundDecoration: foregroundDecoration ?? this.foregroundDecoration, 146 | elevation: elevation ?? this.elevation, 147 | ); 148 | } 149 | 150 | ButtonStyleData get _toButtonStyleData { 151 | return ButtonStyleData( 152 | height: height ?? height, 153 | width: width ?? width, 154 | padding: padding ?? padding, 155 | decoration: decoration ?? decoration, 156 | foregroundDecoration: foregroundDecoration ?? foregroundDecoration, 157 | elevation: elevation ?? elevation, 158 | ); 159 | } 160 | } 161 | 162 | /// A class to configure the theme of the button's icon. 163 | class IconStyleData { 164 | /// Creates an IconStyleData. 165 | /// It's a class to configure the theme of the button's icon. 166 | const IconStyleData({ 167 | this.icon = const Icon(Icons.arrow_drop_down), 168 | this.iconDisabledColor, 169 | this.iconEnabledColor, 170 | this.iconSize = 24, 171 | this.openMenuIcon, 172 | }); 173 | 174 | /// The widget to use for the drop-down button's suffix icon. 175 | /// 176 | /// Defaults to an [Icon] with the [Icons.arrow_drop_down] glyph. 177 | final Widget icon; 178 | 179 | /// The color of any [Icon] descendant of [icon] if this button is disabled, 180 | /// i.e. if [onChanged] is null. 181 | /// 182 | /// Defaults to [MaterialColor.shade400] of [Colors.grey] when the theme's 183 | /// [ThemeData.brightness] is [Brightness.light] and to 184 | /// [Colors.white10] when it is [Brightness.dark] 185 | final Color? iconDisabledColor; 186 | 187 | /// The color of any [Icon] descendant of [icon] if this button is enabled, 188 | /// i.e. if [onChanged] is defined. 189 | /// 190 | /// Defaults to [MaterialColor.shade700] of [Colors.grey] when the theme's 191 | /// [ThemeData.brightness] is [Brightness.light] and to 192 | /// [Colors.white70] when it is [Brightness.dark] 193 | final Color? iconEnabledColor; 194 | 195 | /// The size to use for the drop-down button's icon. 196 | /// 197 | /// Defaults to 24.0. 198 | final double iconSize; 199 | 200 | /// Shows different icon when dropdown menu is open 201 | final Widget? openMenuIcon; 202 | 203 | /// Create a clone of the current [IconStyleData] but with the provided 204 | /// parameters overridden. 205 | IconStyleData copyWith({ 206 | Widget? icon, 207 | Color? iconDisabledColor, 208 | Color? iconEnabledColor, 209 | double? iconSize, 210 | Widget? openMenuIcon, 211 | }) { 212 | return IconStyleData( 213 | icon: icon ?? this.icon, 214 | iconDisabledColor: iconDisabledColor ?? this.iconDisabledColor, 215 | iconEnabledColor: iconEnabledColor ?? this.iconEnabledColor, 216 | iconSize: iconSize ?? this.iconSize, 217 | openMenuIcon: openMenuIcon ?? this.openMenuIcon, 218 | ); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /packages/dropdown_button2/lib/src/dropdown_menu.dart: -------------------------------------------------------------------------------- 1 | part of 'dropdown_button2.dart'; 2 | 3 | SearchMatchFn _defaultSearchMatchFn() => (DropdownItem item, String searchValue) => 4 | item.value.toString().toLowerCase().contains(searchValue.toLowerCase()); 5 | 6 | class _MenuLimits { 7 | const _MenuLimits(this.top, this.bottom, this.height, this.scrollOffset); 8 | 9 | final double top; 10 | final double bottom; 11 | final double height; 12 | final double scrollOffset; 13 | } 14 | 15 | class _DropdownMenu extends StatefulWidget { 16 | const _DropdownMenu({ 17 | super.key, 18 | required this.route, 19 | required this.scrollController, 20 | required this.textDirection, 21 | required this.buttonRect, 22 | required this.constraints, 23 | required this.mediaQueryPadding, 24 | required this.enableFeedback, 25 | }); 26 | 27 | final _DropdownRoute route; 28 | final ScrollController scrollController; 29 | final TextDirection? textDirection; 30 | final Rect buttonRect; 31 | final BoxConstraints constraints; 32 | final EdgeInsets mediaQueryPadding; 33 | final bool enableFeedback; 34 | 35 | @override 36 | _DropdownMenuState createState() => _DropdownMenuState(); 37 | } 38 | 39 | class _DropdownMenuState extends State<_DropdownMenu> { 40 | late final CurvedAnimation _fadeOpacity; 41 | late final CurvedAnimation _resize; 42 | late List _children; 43 | late SearchMatchFn _searchMatchFn; 44 | 45 | List> get items => widget.route.items; 46 | 47 | DropdownStyleData get dropdownStyle => widget.route.dropdownStyle; 48 | 49 | DropdownSearchData? get searchData => widget.route.searchData; 50 | 51 | _DropdownItemButton dropdownItemButton(int index) => _DropdownItemButton( 52 | route: widget.route, 53 | scrollController: widget.scrollController, 54 | textDirection: widget.textDirection, 55 | buttonRect: widget.buttonRect, 56 | constraints: widget.constraints, 57 | mediaQueryPadding: widget.mediaQueryPadding, 58 | itemIndex: index, 59 | enableFeedback: widget.enableFeedback, 60 | ); 61 | 62 | @override 63 | void initState() { 64 | super.initState(); 65 | // We need to hold these animations as state because of their curve 66 | // direction. When the route's animation reverses, if we were to recreate 67 | // the CurvedAnimation objects in build, we'd lose 68 | // CurvedAnimation._curveDirection. 69 | _fadeOpacity = CurvedAnimation( 70 | parent: widget.route.animation!, 71 | curve: const Interval(0.0, 0.25), 72 | reverseCurve: const Interval(0.75, 1.0), 73 | ); 74 | _resize = CurvedAnimation( 75 | parent: widget.route.animation!, 76 | curve: dropdownStyle.openInterval, 77 | reverseCurve: const Threshold(0.0), 78 | ); 79 | //If searchController is null, then it'll perform as a normal dropdown 80 | //and search functions will not be executed. 81 | final searchController = searchData?.searchController; 82 | if (searchController == null) { 83 | _children = [ 84 | for (int index = 0; index < items.length; ++index) dropdownItemButton(index), 85 | ]; 86 | } else { 87 | _searchMatchFn = searchData?.searchMatchFn ?? _defaultSearchMatchFn(); 88 | _children = _getSearchItems(); 89 | // Add listener to searchController (if it's used) to update the shown items. 90 | searchController.addListener(_onSearchChange); 91 | } 92 | } 93 | 94 | void _onSearchChange() { 95 | _children = _getSearchItems(); 96 | setState(() {}); 97 | } 98 | 99 | List _getSearchItems() { 100 | final String currentSearch = searchData!.searchController!.text; 101 | return [ 102 | for (int index = 0; index < items.length; ++index) 103 | if (_searchMatchFn(items[index], currentSearch)) dropdownItemButton(index), 104 | ]; 105 | } 106 | 107 | @override 108 | void dispose() { 109 | _fadeOpacity.dispose(); 110 | _resize.dispose(); 111 | searchData?.searchController?.removeListener(_onSearchChange); 112 | super.dispose(); 113 | } 114 | 115 | final _states = { 116 | WidgetState.dragged, 117 | WidgetState.hovered, 118 | }; 119 | 120 | bool get _isIOS => Theme.of(context).platform == TargetPlatform.iOS; 121 | 122 | ScrollbarThemeData? get _scrollbarTheme => dropdownStyle.scrollbarTheme; 123 | 124 | bool get _iOSThumbVisibility => _scrollbarTheme?.thumbVisibility?.resolve(_states) ?? true; 125 | 126 | bool get _hasIntrinsicHeight => 127 | widget.route.items.any((item) => item.intrinsicHeight) || 128 | (widget.route.dropdownSeparator != null && widget.route.dropdownSeparator!.intrinsicHeight); 129 | 130 | @override 131 | Widget build(BuildContext context) { 132 | // The menu is shown in three stages (unit timing in brackets): 133 | // [0s - 0.25s] - Fade in a rect-sized menu container with the selected item. 134 | // [0.25s - 0.5s] - Grow the otherwise empty menu container from the center 135 | // until it's big enough for as many items as we're going to show. 136 | // [0.5s - 1.0s] Fade in the remaining visible items from top to bottom. 137 | // 138 | // When the menu is dismissed we just fade the entire thing out 139 | // in the first 0.25s. 140 | assert(debugCheckHasMaterialLocalizations(context)); 141 | final MaterialLocalizations localizations = MaterialLocalizations.of(context); 142 | final _DropdownRoute route = widget.route; 143 | 144 | final separator = widget.route.dropdownSeparator; 145 | 146 | final Widget dropdownMenu = Material( 147 | type: MaterialType.transparency, 148 | textStyle: route.style, 149 | child: Column( 150 | mainAxisSize: MainAxisSize.min, 151 | children: [ 152 | if (searchData?.searchBarWidget != null) searchData!.searchBarWidget!, 153 | if (_children.isEmpty && searchData?.noResultsWidget != null) 154 | searchData!.noResultsWidget! 155 | else 156 | Flexible( 157 | // This Material wrapper is temporary until it's fixed by flutter at: 158 | // https://github.com/flutter/flutter/issues/86584 159 | // https://github.com/flutter/flutter/issues/73315 160 | child: Material( 161 | type: MaterialType.transparency, 162 | textStyle: route.style, 163 | child: Padding( 164 | padding: dropdownStyle.scrollPadding ?? EdgeInsets.zero, 165 | child: ScrollConfiguration( 166 | // Dropdown menus should never overscroll or display an overscroll indicator. 167 | // Scrollbars are built-in below. 168 | // Platform must use Theme and ScrollPhysics must be Clamping. 169 | behavior: ScrollConfiguration.of(context).copyWith( 170 | scrollbars: false, 171 | overscroll: false, 172 | physics: const ClampingScrollPhysics(), 173 | platform: Theme.of(context).platform, 174 | ), 175 | child: PrimaryScrollController( 176 | controller: widget.scrollController, 177 | child: Theme( 178 | data: Theme.of(context).copyWith( 179 | scrollbarTheme: dropdownStyle.scrollbarTheme, 180 | ), 181 | child: Scrollbar( 182 | thumbVisibility: 183 | // ignore: avoid_bool_literals_in_conditional_expressions 184 | _isIOS ? _iOSThumbVisibility : true, 185 | thickness: _isIOS ? _scrollbarTheme?.thickness?.resolve(_states) : null, 186 | radius: _isIOS ? _scrollbarTheme?.radius : null, 187 | child: ListView.custom( 188 | // Ensure this always inherits the PrimaryScrollController 189 | primary: true, 190 | shrinkWrap: true, 191 | padding: dropdownStyle.padding ?? kMaterialListPadding, 192 | itemExtentBuilder: _hasIntrinsicHeight 193 | ? null 194 | : (index, dimensions) { 195 | final childrenLength = separator == null 196 | ? _children.length 197 | : SeparatedSliverChildBuilderDelegate 198 | .computeActualChildCount(_children.length); 199 | // TODO(Ahmed): Remove this when https://github.com/flutter/flutter/pull/142428 200 | // is supported by the min version of the package [Flutter>=3.22.0]. 201 | if (index >= childrenLength) { 202 | return 100; 203 | } 204 | return separator == null 205 | ? route.items[index].height 206 | : index.isOdd 207 | ? separator.height 208 | : route.items[index ~/ 2].height; 209 | }, 210 | childrenDelegate: separator == null 211 | ? SliverChildBuilderDelegate( 212 | (context, index) => _children[index], 213 | childCount: _children.length, 214 | ) 215 | : SeparatedSliverChildBuilderDelegate( 216 | itemCount: _children.length, 217 | itemBuilder: (context, index) => _children[index], 218 | separatorBuilder: (context, index) => SizedBox( 219 | height: separator.intrinsicHeight ? null : separator.height, 220 | child: separator, 221 | ), 222 | ), 223 | ), 224 | ), 225 | ), 226 | ), 227 | ), 228 | ), 229 | ), 230 | ), 231 | ], 232 | ), 233 | ); 234 | 235 | return FadeTransition( 236 | opacity: _fadeOpacity, 237 | child: CustomPaint( 238 | painter: _DropdownMenuPainter( 239 | color: Theme.of(context).canvasColor, 240 | elevation: dropdownStyle.elevation, 241 | selectedIndex: route.selectedIndex, 242 | resize: _resize, 243 | itemHeight: items[0].height, 244 | dropdownDecoration: dropdownStyle.decoration, 245 | textDirection: widget.textDirection, 246 | ), 247 | child: Semantics( 248 | scopesRoute: true, 249 | namesRoute: true, 250 | explicitChildNodes: true, 251 | label: localizations.popupMenuLabel, 252 | child: ClipRRect( 253 | //Prevent scrollbar, ripple effect & items from going beyond border boundaries when scrolling. 254 | clipBehavior: 255 | dropdownStyle.decoration?.borderRadius != null ? Clip.antiAlias : Clip.none, 256 | borderRadius: dropdownStyle.decoration?.borderRadius ?? BorderRadius.zero, 257 | child: dropdownStyle.dropdownBuilder?.call(context, dropdownMenu) ?? dropdownMenu, 258 | ), 259 | ), 260 | ), 261 | ); 262 | } 263 | } 264 | 265 | class _DropdownMenuPainter extends CustomPainter { 266 | _DropdownMenuPainter({ 267 | required this.color, 268 | required this.elevation, 269 | required this.selectedIndex, 270 | required this.resize, 271 | required this.itemHeight, 272 | required this.dropdownDecoration, 273 | required this.textDirection, 274 | }) : _painter = dropdownDecoration 275 | ?.copyWith( 276 | color: dropdownDecoration.color ?? color, 277 | boxShadow: dropdownDecoration.boxShadow ?? kElevationToShadow[elevation], 278 | ) 279 | .createBoxPainter(() {}) ?? 280 | BoxDecoration( 281 | // If you add an image here, you must provide a real 282 | // configuration in the paint() function and you must provide some sort 283 | // of onChanged callback here. 284 | color: color, 285 | borderRadius: const BorderRadius.all(Radius.circular(2.0)), 286 | boxShadow: kElevationToShadow[elevation], 287 | ).createBoxPainter(), 288 | super(repaint: resize); 289 | 290 | final Color? color; 291 | final int? elevation; 292 | final int? selectedIndex; 293 | final Animation resize; 294 | final double itemHeight; 295 | final BoxDecoration? dropdownDecoration; 296 | final TextDirection? textDirection; 297 | 298 | final BoxPainter _painter; 299 | 300 | @override 301 | void paint(Canvas canvas, Size size) { 302 | final Tween top = Tween( 303 | //Begin at 0.0 instead of selectedItemOffset so that the menu open animation 304 | //always start from top to bottom instead of starting from the selected item 305 | begin: 0.0, 306 | end: 0.0, 307 | ); 308 | 309 | final Tween bottom = Tween( 310 | begin: clampDouble(top.begin! + itemHeight, math.min(itemHeight, size.height), size.height), 311 | end: size.height, 312 | ); 313 | 314 | final Rect rect = Rect.fromLTRB(0.0, top.evaluate(resize), size.width, bottom.evaluate(resize)); 315 | 316 | _painter.paint( 317 | canvas, 318 | rect.topLeft, 319 | ImageConfiguration(size: rect.size, textDirection: textDirection), 320 | ); 321 | } 322 | 323 | @override 324 | bool shouldRepaint(_DropdownMenuPainter oldPainter) { 325 | return oldPainter.color != color || 326 | oldPainter.elevation != elevation || 327 | oldPainter.selectedIndex != selectedIndex || 328 | oldPainter.itemHeight != itemHeight || 329 | oldPainter.dropdownDecoration != dropdownDecoration || 330 | oldPainter.textDirection != textDirection || 331 | oldPainter.resize != resize; 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /packages/dropdown_button2/lib/src/dropdown_menu_item.dart: -------------------------------------------------------------------------------- 1 | part of 'dropdown_button2.dart'; 2 | 3 | /// Represents an item in a dropdown menu created by a [DropdownButton2]. 4 | /// 5 | /// The type `T` is the type of the value the entry represents. All the entries 6 | /// in a given menu must represent values with consistent types. 7 | class DropdownItem extends _DropdownMenuItemContainer { 8 | /// Creates a dropdown item. 9 | /// 10 | /// The [child] property must be set. 11 | const DropdownItem({ 12 | required super.child, 13 | super.height, 14 | super.intrinsicHeight, 15 | super.alignment, 16 | this.onTap, 17 | this.value, 18 | this.enabled = true, 19 | this.closeOnTap = true, 20 | super.key, 21 | }); 22 | 23 | /// Called when the dropdown menu item is tapped. 24 | final VoidCallback? onTap; 25 | 26 | /// The value to return if the user selects this menu item. 27 | /// 28 | /// Eventually returned in a call to [DropdownButton.onChanged]. 29 | final T? value; 30 | 31 | /// Whether or not a user can select this menu item. 32 | /// 33 | /// Defaults to `true`. 34 | final bool enabled; 35 | 36 | /// Whether the dropdown should close when the item is tapped. 37 | /// 38 | /// Defaults to true. 39 | final bool closeOnTap; 40 | 41 | /// Creates a copy of this DropdownItem but with the given fields replaced with the new values. 42 | DropdownItem copyWith({ 43 | Widget? child, 44 | double? height, 45 | bool? intrinsicHeight, 46 | void Function()? onTap, 47 | T? value, 48 | bool? enabled, 49 | AlignmentGeometry? alignment, 50 | bool? closeOnTap, 51 | }) { 52 | return DropdownItem( 53 | height: height ?? this.height, 54 | intrinsicHeight: intrinsicHeight ?? this.intrinsicHeight, 55 | onTap: onTap ?? this.onTap, 56 | value: value ?? this.value, 57 | enabled: enabled ?? this.enabled, 58 | alignment: alignment ?? this.alignment, 59 | closeOnTap: closeOnTap ?? this.closeOnTap, 60 | child: child ?? this.child, 61 | ); 62 | } 63 | } 64 | 65 | // The container widget for a menu item created by a [DropdownButton2]. It 66 | // provides the default configuration for [DropdownMenuItem]s, as well as a 67 | // [DropdownButton]'s hint and disabledHint widgets. 68 | class _DropdownMenuItemContainer extends StatelessWidget { 69 | /// Creates an item for a dropdown menu. 70 | /// 71 | /// The [child] argument is required. 72 | const _DropdownMenuItemContainer({ 73 | super.key, 74 | this.alignment = AlignmentDirectional.centerStart, 75 | required this.child, 76 | this.height = _kMenuItemHeight, 77 | this.intrinsicHeight = false, 78 | }); 79 | 80 | /// The widget below this widget in the tree. 81 | /// 82 | /// Typically a [Text] widget. 83 | final Widget child; 84 | 85 | /// Defines how the item is positioned within the container. 86 | /// 87 | /// Defaults to [AlignmentDirectional.centerStart]. 88 | /// 89 | /// See also: 90 | /// 91 | /// * [Alignment], a class with convenient constants typically used to 92 | /// specify an [AlignmentGeometry]. 93 | /// * [AlignmentDirectional], like [Alignment] for specifying alignments 94 | /// relative to text direction. 95 | final AlignmentGeometry alignment; 96 | 97 | /// The height of the menu item, default value is [kMinInteractiveDimension] 98 | final double height; 99 | 100 | /// If set to true, then this item's height will vary according to its 101 | /// intrinsic height instead of using [height] property. 102 | /// 103 | /// It is highly recommended to keep this value as false when dealing with 104 | /// a significantly large items list in order to optimize performance. 105 | /// 106 | /// Note: If set to true and there isn't enough vertical room for the menu, there's 107 | /// no way to know the item's intrinsic height in-advance to properly scroll to 108 | /// the selected item. Instead, the provided [height] value will be used, which means 109 | /// the menu's initial scroll offset may not properly scroll to the selected item. 110 | final bool intrinsicHeight; 111 | 112 | @override 113 | Widget build(BuildContext context) { 114 | return Semantics( 115 | button: true, 116 | child: SizedBox( 117 | height: intrinsicHeight ? null : height, 118 | child: Align(alignment: alignment, child: child), 119 | ), 120 | ); 121 | } 122 | } 123 | 124 | // The widget that is the button wrapping the menu items. 125 | class _DropdownItemButton extends StatefulWidget { 126 | const _DropdownItemButton({ 127 | super.key, 128 | required this.route, 129 | required this.scrollController, 130 | required this.textDirection, 131 | required this.buttonRect, 132 | required this.constraints, 133 | required this.mediaQueryPadding, 134 | required this.itemIndex, 135 | required this.enableFeedback, 136 | }); 137 | 138 | final _DropdownRoute route; 139 | final ScrollController scrollController; 140 | final TextDirection? textDirection; 141 | final Rect buttonRect; 142 | final BoxConstraints constraints; 143 | final EdgeInsets mediaQueryPadding; 144 | final int itemIndex; 145 | final bool enableFeedback; 146 | 147 | @override 148 | _DropdownItemButtonState createState() => _DropdownItemButtonState(); 149 | } 150 | 151 | class _DropdownItemButtonState extends State<_DropdownItemButton> { 152 | late CurvedAnimation _opacityAnimation; 153 | 154 | @override 155 | void initState() { 156 | super.initState(); 157 | _setOpacityAnimation(); 158 | } 159 | 160 | @override 161 | void didUpdateWidget(_DropdownItemButton oldWidget) { 162 | super.didUpdateWidget(oldWidget); 163 | if (oldWidget.itemIndex != widget.itemIndex || 164 | oldWidget.route.animation != widget.route.animation || 165 | oldWidget.route.selectedIndex != widget.route.selectedIndex || 166 | widget.route.items.length != oldWidget.route.items.length || 167 | widget.route.dropdownStyle.openInterval.end != 168 | oldWidget.route.dropdownStyle.openInterval.end) { 169 | _opacityAnimation.dispose(); 170 | _setOpacityAnimation(); 171 | } 172 | } 173 | 174 | @override 175 | void dispose() { 176 | _opacityAnimation.dispose(); 177 | super.dispose(); 178 | } 179 | 180 | void _setOpacityAnimation() { 181 | final double menuCurveEnd = widget.route.dropdownStyle.openInterval.end; 182 | final double unit = 0.5 / (widget.route.items.length + 1.5); 183 | final double start = clampDouble(menuCurveEnd + (widget.itemIndex + 1) * unit, 0.0, 1.0); 184 | final double end = clampDouble(start + 1.5 * unit, 0.0, 1.0); 185 | _opacityAnimation = CurvedAnimation( 186 | parent: widget.route.animation!, 187 | curve: Interval(start, end), 188 | ); 189 | } 190 | 191 | void _handleFocusChange(bool focused) { 192 | final bool inTraditionalMode = switch (FocusManager.instance.highlightMode) { 193 | FocusHighlightMode.touch => false, 194 | FocusHighlightMode.traditional => true, 195 | }; 196 | 197 | if (focused && inTraditionalMode) { 198 | final _MenuLimits menuLimits = widget.route.getMenuLimits( 199 | widget.buttonRect, 200 | widget.constraints.maxHeight, 201 | widget.mediaQueryPadding, 202 | widget.itemIndex, 203 | ); 204 | widget.scrollController.animateTo( 205 | menuLimits.scrollOffset, 206 | curve: Curves.easeInOut, 207 | duration: const Duration(milliseconds: 100), 208 | ); 209 | } 210 | } 211 | 212 | void _handleOnTap() { 213 | final DropdownItem dropdownItem = widget.route.items[widget.itemIndex]; 214 | 215 | dropdownItem.onTap?.call(); 216 | widget.route.onChanged?.call(dropdownItem.value); 217 | 218 | if (dropdownItem.closeOnTap) { 219 | Navigator.pop( 220 | context, 221 | _DropdownRouteResult(dropdownItem.value), 222 | ); 223 | } 224 | } 225 | 226 | static const Map _webShortcuts = { 227 | // On the web, up/down don't change focus, *except* in a