Skip to content

Commit cb71135

Browse files
ndonkoHenriCopilotFeodorFitsner
authored
Improve handling of SearchBar suggestions + SemanticsService.get_accessibility_features (#5733)
* feat: `SemanticsService.get_accessibility_features` * fix #2874: improve handling of `SearchBar` suggestions * docs: add product, company, and copyright name sections in publish docs * refactor: remove "Open Search View" button from example * Apply suggestions from code review Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Feodor Fitsner <[email protected]>
1 parent fab56fb commit cb71135

File tree

8 files changed

+450
-135
lines changed

8 files changed

+450
-135
lines changed

packages/flet/lib/src/controls/search_bar.dart

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:flutter/foundation.dart';
12
import 'package:flutter/material.dart';
23

34
import '../extensions/control.dart';
@@ -10,6 +11,7 @@ import '../utils/form_field.dart';
1011
import '../utils/numbers.dart';
1112
import '../utils/text.dart';
1213
import 'base_controls.dart';
14+
import 'control_widget.dart';
1315

1416
class SearchBarControl extends StatefulWidget {
1517
final Control control;
@@ -90,12 +92,6 @@ class _SearchBarControlState extends State<SearchBarControl> {
9092
break;
9193
case "focus":
9294
_focusNode.requestFocus();
93-
break;
94-
case "blur":
95-
// todo: test this method
96-
_focusNode.unfocus(
97-
disposition: UnfocusDisposition.previouslyFocusedChild);
98-
break;
9995
default:
10096
throw Exception("Unknown SearchBar method: $name");
10197
}
@@ -136,8 +132,6 @@ class _SearchBarControlState extends State<SearchBarControl> {
136132

137133
@override
138134
Widget build(BuildContext context) {
139-
debugPrint("SearchAnchor build: ${widget.control.id}");
140-
141135
var value = widget.control.getString("value", "")!;
142136
if (value != _controller.text) {
143137
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -254,9 +248,83 @@ class _SearchBarControlState extends State<SearchBarControl> {
254248
},
255249
suggestionsBuilder:
256250
(BuildContext context, SearchController controller) {
257-
return widget.control.buildWidgets("controls");
251+
return [
252+
_SearchBarSuggestionsHost(control: widget.control),
253+
];
258254
});
259255

260256
return LayoutControl(control: widget.control, child: anchor);
261257
}
262258
}
259+
260+
class _SearchBarSuggestionsHost extends StatefulWidget {
261+
final Control control;
262+
263+
const _SearchBarSuggestionsHost({required this.control});
264+
265+
@override
266+
State<_SearchBarSuggestionsHost> createState() =>
267+
_SearchBarSuggestionsHostState();
268+
}
269+
270+
class _SearchBarSuggestionsHostState extends State<_SearchBarSuggestionsHost> {
271+
late List<Control> _controls;
272+
273+
@override
274+
void initState() {
275+
super.initState();
276+
_controls = widget.control.children("controls");
277+
widget.control.addListener(_handleControlChange);
278+
}
279+
280+
@override
281+
void didUpdateWidget(covariant _SearchBarSuggestionsHost oldWidget) {
282+
super.didUpdateWidget(oldWidget);
283+
if (oldWidget.control != widget.control) {
284+
oldWidget.control.removeListener(_handleControlChange);
285+
_controls = widget.control.children("controls");
286+
widget.control.addListener(_handleControlChange);
287+
}
288+
}
289+
290+
@override
291+
void dispose() {
292+
widget.control.removeListener(_handleControlChange);
293+
super.dispose();
294+
}
295+
296+
void _handleControlChange() {
297+
if (!mounted) return;
298+
299+
var controls = widget.control.children("controls");
300+
301+
// compare ids of current and next controls to avoid unnecessary rebuilds
302+
var currentIds = _controls.map((c) => c.id).toList(growable: false);
303+
var nextIds = controls.map((c) => c.id).toList(growable: false);
304+
if (listEquals(currentIds, nextIds)) {
305+
_controls = controls;
306+
return;
307+
}
308+
309+
// ids differ, update state to trigger rebuild
310+
setState(() {
311+
_controls = controls;
312+
});
313+
}
314+
315+
@override
316+
Widget build(BuildContext context) {
317+
if (_controls.isEmpty) {
318+
return const SizedBox.shrink();
319+
}
320+
321+
return Column(
322+
mainAxisSize: MainAxisSize.min,
323+
crossAxisAlignment: CrossAxisAlignment.stretch,
324+
children: _controls
325+
.map(
326+
(child) => ControlWidget(key: ValueKey(child.id), control: child))
327+
.toList(growable: false),
328+
);
329+
}
330+
}

packages/flet/lib/src/services/semantics_service.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,28 @@ class SemanticsServiceControl extends FletService {
2323

2424
Future<dynamic> _invokeMethod(String name, dynamic args) async {
2525
debugPrint("SemanticsService.$name($args)");
26-
var message = args["message"].toString();
2726
switch (name) {
2827
case "announce_message":
28+
var message = args["message"].toString();
2929
return SemanticsService.announce(
3030
message, args["rtl"] ? TextDirection.rtl : TextDirection.ltr,
3131
assertiveness: control.getAssertiveness(
3232
args["assertiveness"], Assertiveness.polite)!);
3333
case "announce_tooltip":
34+
var message = args["message"].toString();
3435
return SemanticsService.tooltip(message);
36+
case "get_accessibility_features":
37+
var features = SemanticsBinding.instance.accessibilityFeatures;
38+
return {
39+
"accessible_navigation": features.accessibleNavigation,
40+
"bold_text": features.boldText,
41+
"disable_animations": features.disableAnimations,
42+
"high_contrast": features.highContrast,
43+
"invert_colors": features.invertColors,
44+
"reduce_motion": features.reduceMotion,
45+
"on_off_switch_labels": features.onOffSwitchLabels,
46+
"supports_announcements": features.supportsAnnounce,
47+
};
3548
default:
3649
throw Exception("Unknown SemanticsService method: $name");
3750
}

sdk/python/examples/controls/search_bar/basic.py

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,47 @@
11
import flet as ft
22

3+
colors = [
4+
"Amber",
5+
"Blue Grey",
6+
"Brown",
7+
"Deep Orange",
8+
"Green",
9+
"Light Blue",
10+
"Orange",
11+
"Red",
12+
]
13+
314

415
def main(page: ft.Page):
5-
async def handle_tile_click(e: ft.Event[ft.ListTile]):
6-
await anchor.close_view(e.control.title.value)
16+
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
17+
18+
def build_tiles(items: list[str]) -> list[ft.ListTile]:
19+
return [
20+
ft.ListTile(
21+
title=ft.Text(item),
22+
data=item,
23+
on_click=handle_tile_click,
24+
)
25+
for item in items
26+
]
727

8-
async def open_click():
9-
await anchor.open_view()
28+
async def handle_tile_click(e: ft.Event[ft.ListTile]):
29+
await anchor.close_view()
1030

11-
def handle_change(e: ft.Event[ft.SearchBar]):
12-
print(f"handle_change e.data: {e.data}")
31+
async def handle_change(e: ft.Event[ft.SearchBar]):
32+
query = e.control.value.strip().lower()
33+
matching = (
34+
[color for color in colors if query in color.lower()] if query else colors
35+
)
36+
anchor.controls = build_tiles(matching)
1337

1438
def handle_submit(e: ft.Event[ft.SearchBar]):
15-
print(f"handle_submit e.data: {e.data}")
39+
print(f"Submit: {e.data}")
1640

1741
async def handle_tap(e: ft.Event[ft.SearchBar]):
18-
print("handle_tap")
1942
await anchor.open_view()
2043

2144
page.add(
22-
ft.Row(
23-
alignment=ft.MainAxisAlignment.CENTER,
24-
controls=[
25-
ft.OutlinedButton(
26-
content="Open Search View",
27-
on_click=open_click,
28-
),
29-
],
30-
),
3145
anchor := ft.SearchBar(
3246
view_elevation=4,
3347
divider_color=ft.Colors.AMBER,
@@ -36,10 +50,7 @@ async def handle_tap(e: ft.Event[ft.SearchBar]):
3650
on_change=handle_change,
3751
on_submit=handle_submit,
3852
on_tap=handle_tap,
39-
controls=[
40-
ft.ListTile(title=ft.Text(f"Color {i}"), on_click=handle_tile_click)
41-
for i in range(10)
42-
],
53+
controls=build_tiles(colors),
4354
),
4455
)
4556

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import flet as ft
2+
3+
4+
async def main(page: ft.Page):
5+
page.title = "SemanticsService - Accessibility features"
6+
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
7+
page.vertical_alignment = ft.MainAxisAlignment.CENTER
8+
9+
async def get_features() -> str:
10+
features = await ft.SemanticsService().get_accessibility_features()
11+
return "\n".join(
12+
[
13+
f"Accessible navigation: {features.accessible_navigation}",
14+
f"Bold text: {features.bold_text}",
15+
f"Disable animations: {features.disable_animations}",
16+
f"High contrast: {features.high_contrast}",
17+
f"Invert colors: {features.invert_colors}",
18+
f"Reduce motion: {features.reduce_motion}",
19+
f"On/off switch labels: {features.on_off_switch_labels}",
20+
f"Supports announcements: {features.supports_announcements}",
21+
]
22+
)
23+
24+
async def refresh_features(e: ft.Event[ft.Button]):
25+
info.value = await get_features()
26+
27+
page.add(
28+
info := ft.Text(await get_features()),
29+
ft.Button("Refresh features", on_click=refresh_features),
30+
)
31+
32+
33+
ft.run(main)
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
---
22
class_name: flet.SemanticsService
3+
examples: ../../examples/controls/semantics_service
34
---
45

5-
{{ class_all_options(class_name) }}
6+
{{ class_summary(class_name) }}
7+
8+
## Examples
9+
10+
### Retrieve accessibility features
11+
12+
```python
13+
--8<-- "{{ examples }}/accessibility_features.py"
14+
```
15+
16+
{{ class_members(class_name) }}

0 commit comments

Comments
 (0)