Skip to content

Commit e131243

Browse files
mattkaeloic-sharma
andauthored
Update the AccessibilityPlugin::Announce method to account for the view (flutter#172669)
## What's new? - The accessibility "announce" method now takes a `view_id` - The `view_id` is used to lookup the corresponding `FlutterView` in the win32 embedder - Updated the associated tests ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --------- Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com>
1 parent 796fb74 commit e131243

File tree

11 files changed

+114
-36
lines changed

11 files changed

+114
-36
lines changed

dev/integration_tests/new_gallery/lib/pages/backdrop.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,8 @@ class _SettingsIcon extends AnimatedWidget {
264264
child: InkWell(
265265
onTap: () {
266266
toggleSettings();
267-
SemanticsService.announce(
267+
SemanticsService.sendAnnouncement(
268+
View.of(context),
268269
_settingsSemanticLabel(isSettingsOpenNotifier.value, context),
269270
GalleryOptions.of(context).resolvedTextDirection()!,
270271
);

engine/src/flutter/shell/platform/windows/accessibility_plugin.cc

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ static constexpr char kAccessibilityChannelName[] = "flutter/accessibility";
2020
static constexpr char kTypeKey[] = "type";
2121
static constexpr char kDataKey[] = "data";
2222
static constexpr char kMessageKey[] = "message";
23+
static constexpr char kViewIdKey[] = "viewId";
2324
static constexpr char kAnnounceValue[] = "announce";
2425

2526
// Handles messages like:
@@ -61,7 +62,20 @@ void HandleMessage(AccessibilityPlugin* plugin, const EncodableValue& message) {
6162
return;
6263
}
6364

64-
plugin->Announce(*message);
65+
const auto& view_itr = data->find(EncodableValue{kViewIdKey});
66+
if (view_itr == data->end()) {
67+
FML_LOG(ERROR) << "Announce message 'viewId' property is missing.";
68+
return;
69+
}
70+
71+
const auto* view_id_val = std::get_if<FlutterViewId>(&view_itr->second);
72+
if (!view_id_val) {
73+
FML_LOG(ERROR)
74+
<< "Announce message 'viewId' property must be a FlutterViewId.";
75+
return;
76+
}
77+
78+
plugin->Announce(*view_id_val, *message);
6579
} else {
6680
FML_LOG(WARNING) << "Accessibility message type '" << *type
6781
<< "' is not supported.";
@@ -89,14 +103,13 @@ void AccessibilityPlugin::SetUp(BinaryMessenger* binary_messenger,
89103
});
90104
}
91105

92-
void AccessibilityPlugin::Announce(const std::string_view message) {
106+
void AccessibilityPlugin::Announce(const FlutterViewId view_id,
107+
const std::string_view message) {
93108
if (!engine_->semantics_enabled()) {
94109
return;
95110
}
96111

97-
// TODO(loicsharma): Remove implicit view assumption.
98-
// https://github.com/flutter/flutter/issues/142845
99-
auto view = engine_->view(kImplicitViewId);
112+
auto view = engine_->view(view_id);
100113
if (!view) {
101114
return;
102115
}

engine/src/flutter/shell/platform/windows/accessibility_plugin.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
namespace flutter {
1414

15+
using FlutterViewId = int64_t;
1516
class FlutterWindowsEngine;
1617

1718
// Handles messages on the flutter/accessibility channel.
@@ -27,7 +28,8 @@ class AccessibilityPlugin {
2728
AccessibilityPlugin* plugin);
2829

2930
// Announce a message through the assistive technology.
30-
virtual void Announce(const std::string_view message);
31+
virtual void Announce(const FlutterViewId view_id,
32+
const std::string_view message);
3133

3234
private:
3335
// The engine that owns this plugin.

engine/src/flutter/shell/platform/windows/fixtures/main.dart

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ Future<void> sendAccessibilityAnnouncement() async {
6060

6161
// Standard message codec magic number identifiers.
6262
// See: https://github.com/flutter/flutter/blob/ee94fe262b63b0761e8e1f889ae52322fef068d2/packages/flutter/lib/src/services/message_codecs.dart#L262
63-
const int valueMap = 13, valueString = 7;
63+
const int valueMap = 13, valueString = 7, valueInt64 = 4;
6464

65-
// Corresponds to: {"type": "announce", "data": {"message": "hello"}}
65+
// Corresponds to: {"type": "announce", "data": {"viewId": 0, "message": "hello"}}
6666
// See: https://github.com/flutter/flutter/blob/b781da9b5822de1461a769c3b245075359f5464d/packages/flutter/lib/src/semantics/semantics_event.dart#L86
6767
final Uint8List data = Uint8List.fromList([
6868
// Map with 2 entries
@@ -73,8 +73,12 @@ Future<void> sendAccessibilityAnnouncement() async {
7373
valueString, 'announce'.length, ...'announce'.codeUnits,
7474
// Map key: "data"
7575
valueString, 'data'.length, ...'data'.codeUnits,
76-
// Map value: map with 1 entry
77-
valueMap, 1,
76+
// Map value: map with 2 entries
77+
valueMap, 2,
78+
// Map key: "viewId"
79+
valueString, 'viewId'.length, ...'viewId'.codeUnits,
80+
// Map value: 0
81+
valueInt64, 0, 0, 0, 0, 0, 0, 0, 0,
7882
// Map key: "message"
7983
valueString, 'message'.length, ...'message'.codeUnits,
8084
// Map value: "hello"

packages/flutter/lib/src/material/calendar_date_picker.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,8 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
236236
_announcedInitialDate = true;
237237
final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, _selectedDate);
238238
final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : '';
239-
SemanticsService.announce(
239+
SemanticsService.sendAnnouncement(
240+
View.of(context),
240241
'${_localizations.formatFullDate(_selectedDate!)}$semanticLabelSuffix',
241242
_textDirection,
242243
);
@@ -265,7 +266,7 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
265266
DatePickerMode.day => widget.calendarDelegate.formatMonthYear(selected, _localizations),
266267
DatePickerMode.year => widget.calendarDelegate.formatYear(selected.year, _localizations),
267268
};
268-
SemanticsService.announce(message, _textDirection);
269+
SemanticsService.sendAnnouncement(View.of(context), message, _textDirection);
269270
}
270271
});
271272
}
@@ -315,7 +316,8 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
315316
case TargetPlatform.windows:
316317
final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, _selectedDate);
317318
final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : '';
318-
SemanticsService.announce(
319+
SemanticsService.sendAnnouncement(
320+
View.of(context),
319321
'${_localizations.selectedDateLabel} ${widget.calendarDelegate.formatFullDate(_selectedDate!, _localizations)}$semanticLabelSuffix',
320322
_textDirection,
321323
);
@@ -665,7 +667,8 @@ class _MonthPickerState extends State<_MonthPicker> {
665667
// the same day of the month.
666668
_focusedDay = _focusableDayForMonth(_currentMonth, _focusedDay!.day);
667669
}
668-
SemanticsService.announce(
670+
SemanticsService.sendAnnouncement(
671+
View.of(context),
669672
widget.calendarDelegate.formatMonthYear(_currentMonth, _localizations),
670673
_textDirection,
671674
);

packages/flutter/lib/src/material/expansion_tile.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -538,12 +538,12 @@ class _ExpansionTileState extends State<ExpansionTile> {
538538
// semantic announcements on iOS. https://github.com/flutter/flutter/issues/122101.
539539
_timer?.cancel();
540540
_timer = Timer(const Duration(seconds: 1), () {
541-
SemanticsService.announce(stateHint, textDirection);
541+
SemanticsService.sendAnnouncement(View.of(context), stateHint, textDirection);
542542
_timer?.cancel();
543543
_timer = null;
544544
});
545545
} else {
546-
SemanticsService.announce(stateHint, textDirection);
546+
SemanticsService.sendAnnouncement(View.of(context), stateHint, textDirection);
547547
}
548548
widget.onExpansionChanged?.call(_tileController.isExpanded);
549549
}

packages/flutter/lib/src/semantics/semantics_event.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,18 @@ abstract class SemanticsEvent {
9393
/// [1]: https://developer.android.com/reference/android/view/View#announceForAccessibility(java.lang.CharSequence)
9494
///
9595
class AnnounceSemanticsEvent extends SemanticsEvent {
96-
/// Constructs an event that triggers an announcement by the platform.
96+
/// Constructs an event that triggers an announcement by the platform
97+
/// for the provided view.
9798
const AnnounceSemanticsEvent(
9899
this.message,
99-
this.textDirection, {
100+
this.textDirection,
101+
this.viewId, {
100102
this.assertiveness = Assertiveness.polite,
101103
}) : super('announce');
102104

105+
/// The view that this announcement is on.
106+
final int viewId;
107+
103108
/// The message to announce.
104109
final String message;
105110

@@ -117,6 +122,7 @@ class AnnounceSemanticsEvent extends SemanticsEvent {
117122
@override
118123
Map<String, dynamic> getDataMap() {
119124
return <String, dynamic>{
125+
'viewId': viewId,
120126
'message': message,
121127
'textDirection': textDirection.index,
122128
if (assertiveness != Assertiveness.polite) 'assertiveness': assertiveness.index,

packages/flutter/lib/src/semantics/semantics_service.dart

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
/// @docImport 'package:flutter/widgets.dart';
66
library;
77

8-
import 'dart:ui' show TextDirection;
8+
import 'dart:ui' show FlutterView, PlatformDispatcher, TextDirection;
99

1010
import 'package:flutter/services.dart' show SystemChannels;
1111

@@ -23,6 +23,9 @@ export 'dart:ui' show TextDirection;
2323
abstract final class SemanticsService {
2424
/// Sends a semantic announcement.
2525
///
26+
/// This method is deprecated. Prefer using [sendAnnouncement] instead.
27+
///
28+
/// {@template flutter.semantics.service.announce}
2629
/// This should be used for announcement that are not seamlessly announced by
2730
/// the system as a result of a UI state change.
2831
///
@@ -43,15 +46,48 @@ abstract final class SemanticsService {
4346
/// trigger announcements.
4447
///
4548
/// [1]: https://developer.android.com/reference/android/view/View#announceForAccessibility(java.lang.CharSequence)
49+
/// {@endtemplate}
4650
///
51+
@Deprecated(
52+
'Use sendAnnouncement instead. '
53+
'This API is incompatible with multiple windows. '
54+
'This feature was deprecated after v3.35.0-0.1.pre.',
55+
)
4756
static Future<void> announce(
4857
String message,
4958
TextDirection textDirection, {
5059
Assertiveness assertiveness = Assertiveness.polite,
60+
}) async {
61+
final FlutterView? view = PlatformDispatcher.instance.implicitView;
62+
assert(
63+
view != null,
64+
'SemanticsService.announce is incompatible with multiple windows. '
65+
'Use SemanticsService.sendAnnouncement instead.',
66+
);
67+
final AnnounceSemanticsEvent event = AnnounceSemanticsEvent(
68+
message,
69+
textDirection,
70+
view!.viewId,
71+
assertiveness: assertiveness,
72+
);
73+
await SystemChannels.accessibility.send(event.toMap());
74+
}
75+
76+
/// Sends a semantic announcement for a particular view.
77+
///
78+
/// One can use [View.of] to get the current [FlutterView].
79+
///
80+
/// {@macro flutter.semantics.service.announce}
81+
static Future<void> sendAnnouncement(
82+
FlutterView view,
83+
String message,
84+
TextDirection textDirection, {
85+
Assertiveness assertiveness = Assertiveness.polite,
5186
}) async {
5287
final AnnounceSemanticsEvent event = AnnounceSemanticsEvent(
5388
message,
5489
textDirection,
90+
view.viewId,
5591
assertiveness: assertiveness,
5692
);
5793
await SystemChannels.accessibility.send(event.toMap());

packages/flutter/lib/src/widgets/form.dart

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
library;
88

99
import 'dart:async';
10+
import 'dart:ui';
1011

1112
import 'package:flutter/foundation.dart';
1213
import 'package:flutter/rendering.dart';
@@ -22,6 +23,7 @@ import 'pop_scope.dart';
2223
import 'restoration.dart';
2324
import 'restoration_properties.dart';
2425
import 'routes.dart';
26+
import 'view.dart';
2527
import 'will_pop_scope.dart';
2628

2729
// Duration for delay before announcement in IOS so that the announcement won't be interrupted.
@@ -271,10 +273,10 @@ class FormState extends State<Form> {
271273
Widget build(BuildContext context) {
272274
switch (widget.autovalidateMode) {
273275
case AutovalidateMode.always:
274-
_validate();
276+
_validate(View.of(context));
275277
case AutovalidateMode.onUserInteraction:
276278
if (_hasInteractedByUser) {
277-
_validate();
279+
_validate(View.of(context));
278280
}
279281
case AutovalidateMode.onUnfocus:
280282
case AutovalidateMode.disabled:
@@ -335,7 +337,7 @@ class FormState extends State<Form> {
335337
bool validate() {
336338
_hasInteractedByUser = true;
337339
_forceRebuild();
338-
return _validate();
340+
return _validate(View.of(context));
339341
}
340342

341343
/// Validates every [FormField] that is a descendant of this [Form], and
@@ -352,11 +354,11 @@ class FormState extends State<Form> {
352354
final Set<FormFieldState<Object?>> invalidFields = <FormFieldState<Object?>>{};
353355
_hasInteractedByUser = true;
354356
_forceRebuild();
355-
_validate(invalidFields);
357+
_validate(View.of(context), invalidFields);
356358
return invalidFields;
357359
}
358360

359-
bool _validate([Set<FormFieldState<Object?>>? invalidFields]) {
361+
bool _validate(FlutterView view, [Set<FormFieldState<Object?>>? invalidFields]) {
360362
bool hasError = false;
361363
String errorMessage = '';
362364
final bool validateOnFocusChange = widget.autovalidateMode == AutovalidateMode.onUnfocus;
@@ -383,15 +385,17 @@ class FormState extends State<Form> {
383385
unawaited(
384386
Future<void>(() async {
385387
await Future<void>.delayed(_kIOSAnnouncementDelayDuration);
386-
SemanticsService.announce(
388+
SemanticsService.sendAnnouncement(
389+
view,
387390
errorMessage,
388391
directionality,
389392
assertiveness: Assertiveness.assertive,
390393
);
391394
}),
392395
);
393396
} else {
394-
SemanticsService.announce(
397+
SemanticsService.sendAnnouncement(
398+
view,
395399
errorMessage,
396400
directionality,
397401
assertiveness: Assertiveness.assertive,

packages/flutter/test/semantics/semantics_service_test.dart

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import 'package:flutter_test/flutter_test.dart';
99
void main() {
1010
TestWidgetsFlutterBinding.ensureInitialized();
1111

12-
test('Semantic announcement', () async {
12+
testWidgets('Semantic announcement', (WidgetTester tester) async {
1313
final List<Map<dynamic, dynamic>> log = <Map<dynamic, dynamic>>[];
1414

1515
Future<dynamic> handleMessage(dynamic mockMessage) async {
@@ -20,8 +20,9 @@ void main() {
2020
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
2121
.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, handleMessage);
2222

23-
await SemanticsService.announce('announcement 1', TextDirection.ltr);
24-
await SemanticsService.announce(
23+
await SemanticsService.sendAnnouncement(tester.view, 'announcement 1', TextDirection.ltr);
24+
await SemanticsService.sendAnnouncement(
25+
tester.view,
2526
'announcement 2',
2627
TextDirection.rtl,
2728
assertiveness: Assertiveness.assertive,
@@ -31,11 +32,16 @@ void main() {
3132
equals(<Map<String, dynamic>>[
3233
<String, dynamic>{
3334
'type': 'announce',
34-
'data': <String, dynamic>{'message': 'announcement 1', 'textDirection': 1},
35+
'data': <String, dynamic>{
36+
'viewId': tester.view.viewId,
37+
'message': 'announcement 1',
38+
'textDirection': 1,
39+
},
3540
},
3641
<String, dynamic>{
3742
'type': 'announce',
3843
'data': <String, dynamic>{
44+
'viewId': tester.view.viewId,
3945
'message': 'announcement 2',
4046
'textDirection': 0,
4147
'assertiveness': 1,

0 commit comments

Comments
 (0)