Cw 1038 filter transaction popup not scrollable (#2207)

* ui:make overflowing filter sections scrollable * Update pull_request_template.md

Serhii committed Apr 18, 2025 at 15:53 UTC ffe1c115fab2154c7eed4a13a3eb7367db1c24c8
3 files changed +164 -112
.github/pull_request_template.md
+1
@@ -11,3 +11,4 @@ Please include a summary of the changes and which issue is fixed / feature is ad
11 - [ ] Format code
12 - [ ] Look for code duplication
13 - [ ] Clear naming for variables and methods
14 +- [ ] Manual tests in accessibility mode (TalkBack on Android) passed
lib/src/screens/dashboard/widgets/filter_widget.dart
+150 -95
@@ -1,123 +1,178 @@
1 +import 'package:cake_wallet/src/widgets/alert_background.dart';
2 +import 'package:cake_wallet/src/widgets/alert_close_button.dart';
3 import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
4 import 'package:cake_wallet/src/screens/dashboard/widgets/filter_tile.dart';
5 import 'package:cake_wallet/src/widgets/section_divider.dart';
6 import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
7 import 'package:cake_wallet/themes/extensions/menu_theme.dart';
6 -import 'package:cake_wallet/view_model/dashboard/dropdown_filter_item.dart';
7 -import 'package:cake_wallet/view_model/dashboard/dropdown_filter_item_widget.dart';
8 +import 'package:cake_wallet/utils/responsive_layout_util.dart';
9 import 'package:cake_wallet/view_model/dashboard/filter_item.dart';
10 import 'package:flutter/material.dart';
10 -import 'package:cake_wallet/src/widgets/picker_wrapper_widget.dart';
11 import 'package:cake_wallet/generated/i18n.dart';
12 import 'package:flutter_mobx/flutter_mobx.dart';
13 -
14 -//import 'package:date_range_picker/date_range_picker.dart' as date_rage_picker;
13 import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
14
17 -class FilterWidget extends StatelessWidget {
18 - FilterWidget({required this.filterItems});
15 +class FilterWidget extends StatefulWidget {
16 + const FilterWidget({required this.filterItems, this.onClose, Key? key}) : super(key: key);
17
18 final Map<String, List<FilterItem>> filterItems;
19 + final Function()? onClose;
20 +
21 + @override
22 + _FilterWidgetState createState() => _FilterWidgetState();
23 +}
24 +
25 +class _FilterWidgetState extends State<FilterWidget> {
26 + final ScrollController _scrollController = ScrollController();
27 +
28 + @override
29 + void dispose() {
30 + _scrollController.dispose();
31 + super.dispose();
32 + }
33
34 @override
35 Widget build(BuildContext context) {
24 - const sectionDivider = const HorizontalSectionDivider();
25 - return PickerWrapperWidget(
26 - children: [
27 - Padding(
28 - padding: EdgeInsets.only(left: 24, right: 24, top: 24),
29 - child: ClipRRect(
30 - borderRadius: BorderRadius.all(Radius.circular(24)),
31 - child: Container(
32 - color: Theme.of(context).extension<CakeMenuTheme>()!.backgroundColor,
33 - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
34 - Padding(
35 - padding: EdgeInsets.all(24.0),
36 - child: Text(
37 - S.of(context).filter_by,
38 - style: TextStyle(
39 - color:
40 - Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
41 - fontSize: 16,
42 - fontFamily: 'Lato',
43 - decoration: TextDecoration.none,
44 - ),
45 - ),
46 - ),
47 - sectionDivider,
48 - ListView.separated(
49 - padding: EdgeInsets.zero,
50 - shrinkWrap: true,
51 - physics: const NeverScrollableScrollPhysics(),
52 - itemCount: filterItems.length,
53 - separatorBuilder: (context, _) => sectionDivider,
54 - itemBuilder: (_, index1) {
55 - final title = filterItems.keys.elementAt(index1);
56 - final section = filterItems.values.elementAt(index1);
57 - return Column(
58 - crossAxisAlignment: CrossAxisAlignment.start,
59 - children: <Widget>[
60 - Padding(
61 - padding: EdgeInsets.only(top: 20, left: 24, right: 24),
62 - child: Text(
63 - title,
64 - style: TextStyle(
65 - color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
66 - fontSize: 16,
67 - fontFamily: 'Lato',
68 - fontWeight: FontWeight.bold,
69 - decoration: TextDecoration.none),
70 - ),
36 + return AlertBackground(
37 + child: Column(
38 + children: [
39 + const Expanded(child: SizedBox()),
40 + Expanded(
41 + flex: responsiveLayoutUtil.shouldRenderTabletUI ? 16 : 8,
42 + child: LayoutBuilder(
43 + builder: (context, constraints) {
44 + double availableHeight = constraints.maxHeight;
45 + return _buildFilterContent(context, availableHeight);
46 + },
47 + ),
48 + ),
49 + Expanded(
50 + child: AlertCloseButton(
51 + key: const ValueKey('filter_wrapper_close_button_key'),
52 + isPositioned: false,
53 + onTap: widget.onClose,
54 + ),
55 + ),
56 + const SizedBox(height: 24),
57 + ],
58 + ),
59 + );
60 + }
61 +
62 + Widget _buildFilterContent(BuildContext context, double availableHeight) {
63 + const sectionDivider = HorizontalSectionDivider();
64 +
65 + const double totalHeaderHeight = 73;
66 + const double filterTileMinHeight = 40;
67 + double availableHeightForItems = availableHeight - totalHeaderHeight;
68 +
69 + return Center(
70 + child: Column(
71 + mainAxisSize: MainAxisSize.min,
72 + children: [
73 + Padding(
74 + padding: const EdgeInsets.only(left: 24, right: 24, top: 24),
75 + child: ClipRRect(
76 + borderRadius: BorderRadius.circular(24),
77 + child: Container(
78 + color: Theme.of(context).extension<CakeMenuTheme>()!.backgroundColor,
79 + child: Column(
80 + crossAxisAlignment: CrossAxisAlignment.start,
81 + children: [
82 + Padding(
83 + padding: const EdgeInsets.all(24.0),
84 + child: Text(
85 + S.of(context).filter_by,
86 + style: TextStyle(
87 + color: Theme.of(context)
88 + .extension<TransactionTradeTheme>()!
89 + .detailsTitlesColor,
90 + fontSize: 16,
91 + fontFamily: 'Lato',
92 + decoration: TextDecoration.none,
93 ),
72 - ListView.builder(
73 - padding: EdgeInsets.symmetric(horizontal: 28.0),
74 - shrinkWrap: true,
75 - physics: const NeverScrollableScrollPhysics(),
94 + ),
95 + ),
96 + sectionDivider,
97 + ListView.separated(
98 + padding: EdgeInsets.zero,
99 + shrinkWrap: true,
100 + physics: const NeverScrollableScrollPhysics(),
101 + itemCount: widget.filterItems.length,
102 + separatorBuilder: (context, _) => sectionDivider,
103 + itemBuilder: (_, index1) {
104 + final title = widget.filterItems.keys.elementAt(index1);
105 + final section = widget.filterItems.values.elementAt(index1);
106 +
107 + final double itemHeight =
108 + availableHeightForItems / widget.filterItems.length;
109 +
110 + final isSectionScrollable =
111 + (itemHeight < (section.length * filterTileMinHeight));
112 +
113 + final Widget sectionListView = ListView.builder(
114 + controller: isSectionScrollable ? _scrollController : null,
115 + padding: const EdgeInsets.symmetric(horizontal: 28.0),
116 + shrinkWrap: isSectionScrollable ? false : true,
117 + physics: isSectionScrollable
118 + ? const BouncingScrollPhysics()
119 + : const NeverScrollableScrollPhysics(),
120 itemCount: section.length,
121 itemBuilder: (_, index2) {
122 final item = section[index2];
79 -
80 - if (item is DropdownFilterItem) {
81 - return Padding(
82 - padding: EdgeInsets.fromLTRB(8, 0, 8, 16),
83 - child: Container(
84 - decoration: BoxDecoration(
85 - border: Border(
86 - bottom: BorderSide(
87 - width: 1.0,
88 - color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor),
89 - ),
90 - ),
91 - child: DropdownFilterList(
92 - items: item.items,
93 - caption: item.caption,
94 - selectedItem: item.selectedItem,
95 - onItemSelected: item.onItemSelected,
96 - ),
97 - ),
98 - );
99 - }
123 final content = Observer(
101 - builder: (_) => StandardCheckbox(
102 - value: item.value(),
103 - caption: item.caption,
104 - gradientBackground: true,
105 - borderColor: Theme.of(context).dividerColor,
106 - iconColor: Colors.white,
107 - onChanged: (value) => item.onChanged(),
108 - ));
109 - return FilterTile(child: content);
124 + builder: (_) => StandardCheckbox(
125 + value: item.value(),
126 + caption: item.caption,
127 + gradientBackground: true,
128 + borderColor: Theme.of(context).dividerColor,
129 + iconColor: Colors.white,
130 + onChanged: (value) => item.onChanged(),
131 + ),
132 + );
133 + return FilterTile(
134 + child: content,
135 + );
136 },
111 - )
112 - ],
113 - );
114 - },
137 + );
138 +
139 + return Column(
140 + crossAxisAlignment: CrossAxisAlignment.start,
141 + children: [
142 + Padding(
143 + padding: const EdgeInsets.only(top: 20, left: 24, right: 24),
144 + child: Text(
145 + title,
146 + style: TextStyle(
147 + color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
148 + fontSize: 16,
149 + fontFamily: 'Lato',
150 + fontWeight: FontWeight.bold,
151 + decoration: TextDecoration.none,
152 + ),
153 + ),
154 + ),
155 + Container(
156 + height: isSectionScrollable ? itemHeight - totalHeaderHeight : null,
157 + child: isSectionScrollable
158 + ? Scrollbar(
159 + controller: _scrollController,
160 + thumbVisibility: true,
161 + child: sectionListView,
162 + )
163 + : sectionListView,
164 + ),
165 + ],
166 + );
167 + },
168 + ),
169 + ],
170 ),
116 - ]),
171 + ),
172 ),
173 ),
119 - )
120 - ],
174 + ],
175 + ),
176 );
177 }
178 }
lib/src/widgets/alert_close_button.dart
+13 -17
@@ -7,6 +7,7 @@ class AlertCloseButton extends StatelessWidget {
7 this.image,
8 this.bottom,
9 this.onTap,
10 + this.isPositioned = true,
11 super.key,
12 });
13
@@ -14,6 +15,7 @@ class AlertCloseButton extends StatelessWidget {
15
16 final Image? image;
17 final double? bottom;
18 + final bool isPositioned;
19
20 final closeButton = Image.asset(
21 'assets/images/close.png',
@@ -22,24 +24,18 @@ class AlertCloseButton extends StatelessWidget {
24
25 @override
26 Widget build(BuildContext context) {
25 - return Positioned(
26 - bottom: bottom ?? 60,
27 - child: GestureDetector(
27 + final button = GestureDetector(
28 onTap: onTap ?? () => Navigator.of(context).pop(),
29 child: Semantics(
30 - label: S.of(context).close,
31 - button: true,
32 - enabled: true,
33 - child: Container(
34 - height: 42,
35 - width: 42,
36 - decoration: BoxDecoration(color: Colors.white, shape: BoxShape.circle),
37 - child: Center(
38 - child: image ?? closeButton,
39 - ),
40 - ),
41 - ),
42 - ),
43 - );
30 + label: S.of(context).close,
31 + button: true,
32 + enabled: true,
33 + child: Container(
34 + height: 42,
35 + width: 42,
36 + decoration: BoxDecoration(color: Colors.white, shape: BoxShape.circle),
37 + child: Center(child: image ?? closeButton))));
38 +
39 + return isPositioned ? Positioned(bottom: bottom ?? 60, child: button) : button;
40 }
41 }