Skip to content

Commit 9f83182

Browse files
authored
feat: multiple select dropdown (#198)
1 parent 0ccaaf7 commit 9f83182

File tree

9 files changed

+184
-13
lines changed

9 files changed

+184
-13
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ mason-lock.json
2222
# The .vscode folder contains launch configuration and tasks you configure in
2323
# VS Code which you may wish to be included in version control, so this line
2424
# is commented out by default.
25+
.vscode/
2526
.vscode/settings.json
2627

2728
# Flutter/Dart/Pub related

design_system/.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ migrate_working_dir/
1919
# The .vscode folder contains launch configuration and tasks you configure in
2020
# VS Code which you may wish to be included in version control, so this line
2121
# is commented out by default.
22-
#.vscode/
22+
.vscode/
2323

2424
# Flutter/Dart/Pub related
2525
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.

design_system/design_system_gallery/.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ migrate_working_dir/
1919
# The .vscode folder contains launch configuration and tasks you configure in
2020
# VS Code which you may wish to be included in version control, so this line
2121
# is commented out by default.
22-
#.vscode/
22+
.vscode/
2323

2424
# Flutter/Dart/Pub related
2525
**/doc/api/

design_system/design_system_gallery/ios/Podfile.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ SPEC CHECKSUMS:
2626

2727
PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189
2828

29-
COCOAPODS: 1.12.1
29+
COCOAPODS: 1.14.3

design_system/design_system_gallery/lib/gallery/gallery_app_dropdown_screen.dart

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
//ignore_for_file: unused-files, unused-code
12
import 'package:auto_route/auto_route.dart';
2-
import 'package:design_system/widgets/app_dropdown.dart';
3+
import 'package:design_system/widgets/app_select_dropdown.dart';
34
import 'package:design_system_gallery/gallery/gallery_scaffold_screen.dart';
45
import 'package:flutter/material.dart';
6+
import 'package:flutter_screenutil/flutter_screenutil.dart';
57

68
@RoutePage()
79
class GalleryDropdownScreen extends StatelessWidget {
@@ -14,17 +16,18 @@ class GalleryDropdownScreen extends StatelessWidget {
1416
margin: const EdgeInsets.all(20),
1517
child: Column(
1618
children: [
17-
AppDropdownMenu<int>(
18-
initialValue: 1,
19-
dropdownMenuEntries: const [
19+
SizedBox(height: 30.h),
20+
AppSelectDropdown<int>(
21+
label: 'Select',
22+
items: const [
2023
(value: 1, label: 'Option 1'),
2124
(value: 2, label: 'Option 2'),
2225
(value: 3, label: 'Option 3'),
2326
(value: 4, label: 'Option 4'),
2427
(value: 5, label: 'Option 5'),
2528
(value: 6, label: 'Option 6'),
2629
],
27-
onSelected: (int? value) {},
30+
onChanged: (int? value) {},
2831
),
2932
],
3033
),

design_system/lib/design_system.dart

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
library design_system;
22

3-
export 'theme/custom_colors.dart';
4-
export 'theme/app_text_styles.dart';
3+
export 'extensions/context_extensions.dart';
54
export 'theme/app_dimensions.dart';
5+
export 'theme/app_text_styles.dart';
66
export 'theme/app_theme.dart';
7-
export 'extensions/context_extensions.dart';
7+
export 'theme/custom_colors.dart';
8+
export 'widgets/app_select_dropdown.dart';

design_system/lib/theme/app_text_styles.dart

-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ class AppTextStyles extends TextTheme {
4848
GoogleFonts.roboto(
4949
fontSize: fontSize,
5050
fontWeight: fontWeight,
51-
color: Colors.white,
5251
);
5352

5453
static AppTextStyles getDefaultAppStyles() => AppTextStyles.fromTextTheme(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import 'package:design_system/design_system.dart';
2+
import 'package:design_system/extensions/color_extensions.dart';
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_screenutil/flutter_screenutil.dart';
5+
6+
typedef MenuItems<T> = ({T value, String label});
7+
8+
const _kAnimationDuration = Duration(milliseconds: 200);
9+
10+
class AppSelectDropdown<T> extends StatefulWidget {
11+
final double? width;
12+
final String label;
13+
final List<MenuItems<T>> items;
14+
final Function(T value)? onChanged;
15+
16+
const AppSelectDropdown({
17+
required this.items,
18+
required this.label,
19+
this.onChanged,
20+
this.width,
21+
super.key,
22+
});
23+
24+
@override
25+
State<AppSelectDropdown<T>> createState() => _AppSelectDropdownState<T>();
26+
}
27+
28+
class _AppSelectDropdownState<T> extends State<AppSelectDropdown<T>>
29+
with SingleTickerProviderStateMixin {
30+
late final FocusNode _buttonFocusNode;
31+
final MenuController _menuController = MenuController();
32+
final List<MenuItems<T>> _selectedValues = [];
33+
late AnimationController _animationController;
34+
static final Animatable<double> _easeInTween =
35+
CurveTween(curve: Curves.easeIn);
36+
static final Animatable<double> _halfTween =
37+
Tween<double>(begin: 0.0, end: 0.5);
38+
39+
// ignore: unused_field
40+
late Animation<double> _iconTurns;
41+
@override
42+
void initState() {
43+
_buttonFocusNode = FocusNode(debugLabel: 'Menu Button-${widget.label}');
44+
super.initState();
45+
_animationController = AnimationController(
46+
duration: _kAnimationDuration,
47+
vsync: this,
48+
);
49+
50+
_iconTurns = _animationController.drive(_halfTween.chain(_easeInTween));
51+
}
52+
53+
@override
54+
void dispose() {
55+
_buttonFocusNode.dispose();
56+
_animationController.dispose();
57+
super.dispose();
58+
}
59+
60+
@override
61+
Widget build(BuildContext context) => MenuAnchor(
62+
childFocusNode: _buttonFocusNode,
63+
menuChildren: widget.items
64+
.map(
65+
(elem) => CheckboxMenuButton(
66+
closeOnActivate: false,
67+
value: _selectedValues.contains(elem),
68+
onChanged: (value) {
69+
widget.onChanged?.call(elem.value);
70+
setState(() {
71+
if (value!) {
72+
_selectedValues.add(elem);
73+
} else {
74+
_selectedValues.remove(elem);
75+
}
76+
});
77+
},
78+
child: Container(
79+
width: _calculateWidth(),
80+
constraints: BoxConstraints(
81+
minWidth: 90.w,
82+
),
83+
child: Text(
84+
elem.label,
85+
style: context.theme.textStyles.bodyMedium,
86+
overflow: TextOverflow.ellipsis,
87+
maxLines: 1,
88+
),
89+
),
90+
),
91+
)
92+
.toList(),
93+
controller: _menuController,
94+
builder: (
95+
BuildContext context,
96+
MenuController controller,
97+
Widget? child,
98+
) =>
99+
Container(
100+
constraints: BoxConstraints(
101+
minWidth: 90.w,
102+
maxWidth: 1.sw,
103+
),
104+
decoration: BoxDecoration(
105+
color: context.theme.colorScheme.surface.getShade(100),
106+
borderRadius: BorderRadius.circular(4.r),
107+
border: Border.all(
108+
color: context.theme.colorScheme.onSurface.getShade(200),
109+
),
110+
),
111+
child: InkWell(
112+
onTap: () {
113+
if (controller.isOpen) {
114+
controller.close();
115+
_animationController.reverse();
116+
} else {
117+
controller.open();
118+
_animationController.forward();
119+
}
120+
},
121+
child: Row(
122+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
123+
mainAxisSize: MainAxisSize.min,
124+
children: [
125+
Flexible(
126+
child: Container(
127+
width: widget.width ?? 1.sw,
128+
padding: EdgeInsets.all(8.sp),
129+
child: Text(
130+
_selectedValues.isEmpty
131+
? widget.label
132+
: _selectedValues.map((e) => e.label).join(', '),
133+
overflow: TextOverflow.ellipsis,
134+
maxLines: 1,
135+
style: context.theme.textStyles.labelMedium,
136+
),
137+
),
138+
),
139+
AnimatedBuilder(
140+
animation: _animationController,
141+
builder: (context, child) => Padding(
142+
padding: EdgeInsets.all(8.sp),
143+
child: RotationTransition(
144+
turns: _iconTurns,
145+
child: Icon(
146+
Icons.expand_more,
147+
color: context.theme.customColors.textColor,
148+
),
149+
),
150+
),
151+
),
152+
],
153+
),
154+
),
155+
),
156+
);
157+
158+
double _calculateWidth() {
159+
final width = widget.width;
160+
if (width == null) return 1.sw * .761;
161+
if (width <= .3.sw) return width * .9;
162+
if (width <= .5.sw) return width * .93;
163+
if (width <= .8.sw) return width * .95;
164+
if (width < 1.sw) return width * .8;
165+
return width * .761;
166+
}
167+
}

lib/ui/main/main_screen.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import 'package:design_system/theme/app_theme.dart';
22
import 'package:flutter/material.dart';
3+
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
34
import 'package:flutter_localizations/flutter_localizations.dart';
45
import 'package:flutter_template/core/di/di_provider.dart';
56
import 'package:flutter_template/ui/resources.dart';
67
import 'package:flutter_template/ui/router/app_router.dart';
7-
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
88

99
class MainScreen extends StatelessWidget {
1010
const MainScreen({super.key});

0 commit comments

Comments
 (0)