From 29aa8c90aaad0bb5c29f80dc586ba95bdeb2028b Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Thu, 24 Jul 2025 09:50:40 -0400 Subject: [PATCH 01/23] Update the AccessibilityPlugin::Announce method to account for the view --- .../new_gallery/lib/pages/backdrop.dart | 1 + .../platform/windows/accessibility_plugin.cc | 19 ++++++++++++++----- .../platform/windows/accessibility_plugin.h | 4 +++- .../shell/platform/windows/fixtures/main.dart | 12 ++++++++---- .../src/material/calendar_date_picker.dart | 5 ++++- .../lib/src/material/expansion_tile.dart | 4 ++-- .../lib/src/semantics/semantics_event.dart | 5 +++++ .../lib/src/semantics/semantics_service.dart | 2 ++ packages/flutter/lib/src/widgets/form.dart | 13 ++++++++----- .../semantics/semantics_service_test.dart | 6 ++++-- .../flutter_test/test/widget_tester_test.dart | 10 ++++++++-- 11 files changed, 59 insertions(+), 22 deletions(-) diff --git a/dev/integration_tests/new_gallery/lib/pages/backdrop.dart b/dev/integration_tests/new_gallery/lib/pages/backdrop.dart index dd423af0c9435..a268b0c342a0f 100644 --- a/dev/integration_tests/new_gallery/lib/pages/backdrop.dart +++ b/dev/integration_tests/new_gallery/lib/pages/backdrop.dart @@ -265,6 +265,7 @@ class _SettingsIcon extends AnimatedWidget { onTap: () { toggleSettings(); SemanticsService.announce( + View.of(context).viewId, _settingsSemanticLabel(isSettingsOpenNotifier.value, context), GalleryOptions.of(context).resolvedTextDirection()!, ); diff --git a/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc b/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc index a1fe9fc21d060..18247a488481c 100644 --- a/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc +++ b/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc @@ -20,6 +20,7 @@ static constexpr char kAccessibilityChannelName[] = "flutter/accessibility"; static constexpr char kTypeKey[] = "type"; static constexpr char kDataKey[] = "data"; static constexpr char kMessageKey[] = "message"; +static constexpr char kViewIdKey[] = "viewId"; static constexpr char kAnnounceValue[] = "announce"; // Handles messages like: @@ -61,7 +62,16 @@ void HandleMessage(AccessibilityPlugin* plugin, const EncodableValue& message) { return; } - plugin->Announce(*message); + const auto& view_itr = data->find(EncodableValue{kViewIdKey}); + if (view_itr == data->end()) { + return; + } + const auto* view_id = std::get_if(&view_itr->second); + if (!view_id) { + return; + } + + plugin->Announce(*view_id, *message); } else { FML_LOG(WARNING) << "Accessibility message type '" << *type << "' is not supported."; @@ -89,14 +99,13 @@ void AccessibilityPlugin::SetUp(BinaryMessenger* binary_messenger, }); } -void AccessibilityPlugin::Announce(const std::string_view message) { +void AccessibilityPlugin::Announce(const FlutterViewId view_id, + const std::string_view message) { if (!engine_->semantics_enabled()) { return; } - // TODO(loicsharma): Remove implicit view assumption. - // https://github.com/flutter/flutter/issues/142845 - auto view = engine_->view(kImplicitViewId); + auto view = engine_->view(view_id); if (!view) { return; } diff --git a/engine/src/flutter/shell/platform/windows/accessibility_plugin.h b/engine/src/flutter/shell/platform/windows/accessibility_plugin.h index cd0c24ead8645..55a501df82a28 100644 --- a/engine/src/flutter/shell/platform/windows/accessibility_plugin.h +++ b/engine/src/flutter/shell/platform/windows/accessibility_plugin.h @@ -12,6 +12,7 @@ namespace flutter { +using FlutterViewId = int64_t; class FlutterWindowsEngine; // Handles messages on the flutter/accessibility channel. @@ -27,7 +28,8 @@ class AccessibilityPlugin { AccessibilityPlugin* plugin); // Announce a message through the assistive technology. - virtual void Announce(const std::string_view message); + virtual void Announce(const FlutterViewId view_id, + const std::string_view message); private: // The engine that owns this plugin. diff --git a/engine/src/flutter/shell/platform/windows/fixtures/main.dart b/engine/src/flutter/shell/platform/windows/fixtures/main.dart index 8a7a5db8d94bc..7bb336522d1da 100644 --- a/engine/src/flutter/shell/platform/windows/fixtures/main.dart +++ b/engine/src/flutter/shell/platform/windows/fixtures/main.dart @@ -60,9 +60,9 @@ Future sendAccessibilityAnnouncement() async { // Standard message codec magic number identifiers. // See: https://github.com/flutter/flutter/blob/ee94fe262b63b0761e8e1f889ae52322fef068d2/packages/flutter/lib/src/services/message_codecs.dart#L262 - const int valueMap = 13, valueString = 7; + const int valueMap = 13, valueString = 7, valueInt64 = 4; - // Corresponds to: {"type": "announce", "data": {"message": "hello"}} + // Corresponds to: {"type": "announce", "data": {"viewId": 0, "message": "hello"}} // See: https://github.com/flutter/flutter/blob/b781da9b5822de1461a769c3b245075359f5464d/packages/flutter/lib/src/semantics/semantics_event.dart#L86 final Uint8List data = Uint8List.fromList([ // Map with 2 entries @@ -73,8 +73,12 @@ Future sendAccessibilityAnnouncement() async { valueString, 'announce'.length, ...'announce'.codeUnits, // Map key: "data" valueString, 'data'.length, ...'data'.codeUnits, - // Map value: map with 1 entry - valueMap, 1, + // Map value: map with 2 entries + valueMap, 2, + // Map key: "viewId" + valueString, 'viewId'.length, ...'viewId'.codeUnits, + // Map value: 0 + valueInt64, 0, 0, 0, 0, 0, 0, 0, 0, // Map key: "message" valueString, 'message'.length, ...'message'.codeUnits, // Map value: "hello" diff --git a/packages/flutter/lib/src/material/calendar_date_picker.dart b/packages/flutter/lib/src/material/calendar_date_picker.dart index d9d877225a567..8a7741039ecef 100644 --- a/packages/flutter/lib/src/material/calendar_date_picker.dart +++ b/packages/flutter/lib/src/material/calendar_date_picker.dart @@ -236,6 +236,7 @@ class _CalendarDatePickerState extends State { final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, _selectedDate); final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : ''; SemanticsService.announce( + View.of(context).viewId, '${_localizations.formatFullDate(_selectedDate!)}$semanticLabelSuffix', _textDirection, ); @@ -264,7 +265,7 @@ class _CalendarDatePickerState extends State { DatePickerMode.day => widget.calendarDelegate.formatMonthYear(selected, _localizations), DatePickerMode.year => widget.calendarDelegate.formatYear(selected.year, _localizations), }; - SemanticsService.announce(message, _textDirection); + SemanticsService.announce(View.of(context).viewId, message, _textDirection); } }); } @@ -315,6 +316,7 @@ class _CalendarDatePickerState extends State { final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, _selectedDate); final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : ''; SemanticsService.announce( + View.of(context).viewId, '${_localizations.selectedDateLabel} ${widget.calendarDelegate.formatFullDate(_selectedDate!, _localizations)}$semanticLabelSuffix', _textDirection, ); @@ -665,6 +667,7 @@ class _MonthPickerState extends State<_MonthPicker> { _focusedDay = _focusableDayForMonth(_currentMonth, _focusedDay!.day); } SemanticsService.announce( + View.of(context).viewId, widget.calendarDelegate.formatMonthYear(_currentMonth, _localizations), _textDirection, ); diff --git a/packages/flutter/lib/src/material/expansion_tile.dart b/packages/flutter/lib/src/material/expansion_tile.dart index 9e3086f4124f5..5116a96f4c8f9 100644 --- a/packages/flutter/lib/src/material/expansion_tile.dart +++ b/packages/flutter/lib/src/material/expansion_tile.dart @@ -538,12 +538,12 @@ class _ExpansionTileState extends State { // semantic announcements on iOS. https://github.com/flutter/flutter/issues/122101. _timer?.cancel(); _timer = Timer(const Duration(seconds: 1), () { - SemanticsService.announce(stateHint, textDirection); + SemanticsService.announce(View.of(context).viewId, stateHint, textDirection); _timer?.cancel(); _timer = null; }); } else { - SemanticsService.announce(stateHint, textDirection); + SemanticsService.announce(View.of(context).viewId, stateHint, textDirection); } widget.onExpansionChanged?.call(_tileController.isExpanded); } diff --git a/packages/flutter/lib/src/semantics/semantics_event.dart b/packages/flutter/lib/src/semantics/semantics_event.dart index 9b9e034d04491..ed24f5c35867e 100644 --- a/packages/flutter/lib/src/semantics/semantics_event.dart +++ b/packages/flutter/lib/src/semantics/semantics_event.dart @@ -95,11 +95,15 @@ abstract class SemanticsEvent { class AnnounceSemanticsEvent extends SemanticsEvent { /// Constructs an event that triggers an announcement by the platform. const AnnounceSemanticsEvent( + this.viewId, this.message, this.textDirection, { this.assertiveness = Assertiveness.polite, }) : super('announce'); + /// The id of the view that this announcement is on. + final int viewId; + /// The message to announce. final String message; @@ -117,6 +121,7 @@ class AnnounceSemanticsEvent extends SemanticsEvent { @override Map getDataMap() { return { + 'viewId': viewId, 'message': message, 'textDirection': textDirection.index, if (assertiveness != Assertiveness.polite) 'assertiveness': assertiveness.index, diff --git a/packages/flutter/lib/src/semantics/semantics_service.dart b/packages/flutter/lib/src/semantics/semantics_service.dart index 750e887e39758..176dc814f7847 100644 --- a/packages/flutter/lib/src/semantics/semantics_service.dart +++ b/packages/flutter/lib/src/semantics/semantics_service.dart @@ -45,11 +45,13 @@ abstract final class SemanticsService { /// [1]: https://developer.android.com/reference/android/view/View#announceForAccessibility(java.lang.CharSequence) /// static Future announce( + int viewId, String message, TextDirection textDirection, { Assertiveness assertiveness = Assertiveness.polite, }) async { final AnnounceSemanticsEvent event = AnnounceSemanticsEvent( + viewId, message, textDirection, assertiveness: assertiveness, diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart index 5e2c07bebd214..657b8aba10779 100644 --- a/packages/flutter/lib/src/widgets/form.dart +++ b/packages/flutter/lib/src/widgets/form.dart @@ -22,6 +22,7 @@ import 'pop_scope.dart'; import 'restoration.dart'; import 'restoration_properties.dart'; import 'routes.dart'; +import 'view.dart'; import 'will_pop_scope.dart'; // Duration for delay before announcement in IOS so that the announcement won't be interrupted. @@ -271,10 +272,10 @@ class FormState extends State
{ Widget build(BuildContext context) { switch (widget.autovalidateMode) { case AutovalidateMode.always: - _validate(); + _validate(View.of(context).viewId); case AutovalidateMode.onUserInteraction: if (_hasInteractedByUser) { - _validate(); + _validate(View.of(context).viewId); } case AutovalidateMode.onUnfocus: case AutovalidateMode.disabled: @@ -335,7 +336,7 @@ class FormState extends State { bool validate() { _hasInteractedByUser = true; _forceRebuild(); - return _validate(); + return _validate(View.of(context).viewId); } /// Validates every [FormField] that is a descendant of this [Form], and @@ -352,11 +353,11 @@ class FormState extends State { final Set> invalidFields = >{}; _hasInteractedByUser = true; _forceRebuild(); - _validate(invalidFields); + _validate(View.of(context).viewId, invalidFields); return invalidFields; } - bool _validate([Set>? invalidFields]) { + bool _validate(int viewId, [Set>? invalidFields]) { bool hasError = false; String errorMessage = ''; final bool validateOnFocusChange = widget.autovalidateMode == AutovalidateMode.onUnfocus; @@ -384,6 +385,7 @@ class FormState extends State { Future(() async { await Future.delayed(_kIOSAnnouncementDelayDuration); SemanticsService.announce( + viewId, errorMessage, directionality, assertiveness: Assertiveness.assertive, @@ -392,6 +394,7 @@ class FormState extends State { ); } else { SemanticsService.announce( + viewId, errorMessage, directionality, assertiveness: Assertiveness.assertive, diff --git a/packages/flutter/test/semantics/semantics_service_test.dart b/packages/flutter/test/semantics/semantics_service_test.dart index f741f316e5f1c..bae5cecd4124b 100644 --- a/packages/flutter/test/semantics/semantics_service_test.dart +++ b/packages/flutter/test/semantics/semantics_service_test.dart @@ -20,8 +20,9 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockDecodedMessageHandler(SystemChannels.accessibility, handleMessage); - await SemanticsService.announce('announcement 1', TextDirection.ltr); + await SemanticsService.announce(1, 'announcement 1', TextDirection.ltr); await SemanticsService.announce( + 2, 'announcement 2', TextDirection.rtl, assertiveness: Assertiveness.assertive, @@ -31,11 +32,12 @@ void main() { equals(>[ { 'type': 'announce', - 'data': {'message': 'announcement 1', 'textDirection': 1}, + 'data': {'viewId': 1, 'message': 'announcement 1', 'textDirection': 1}, }, { 'type': 'announce', 'data': { + 'viewId': 2, 'message': 'announcement 2', 'textDirection': 0, 'assertiveness': 1, diff --git a/packages/flutter_test/test/widget_tester_test.dart b/packages/flutter_test/test/widget_tester_test.dart index 09e940561d86b..d6684d50e87db 100644 --- a/packages/flutter_test/test/widget_tester_test.dart +++ b/packages/flutter_test/test/widget_tester_test.dart @@ -701,26 +701,30 @@ void main() { isFalse, ); - await SemanticsService.announce('announcement 1', TextDirection.ltr); + await SemanticsService.announce(0, 'announcement 1', TextDirection.ltr); await SemanticsService.announce( + 0, 'announcement 2', TextDirection.rtl, assertiveness: Assertiveness.assertive, ); - await SemanticsService.announce('announcement 3', TextDirection.rtl); + await SemanticsService.announce(0, 'announcement 3', TextDirection.rtl); final List list = tester.takeAnnouncements(); expect(list, hasLength(3)); final CapturedAccessibilityAnnouncement first = list[0]; + expect(first.viewId, 0); expect(first.message, 'announcement 1'); expect(first.textDirection, TextDirection.ltr); final CapturedAccessibilityAnnouncement second = list[1]; + expect(second.viewId, 0); expect(second.message, 'announcement 2'); expect(second.textDirection, TextDirection.rtl); expect(second.assertiveness, Assertiveness.assertive); final CapturedAccessibilityAnnouncement third = list[2]; + expect(third.viewId, 0); expect(third.message, 'announcement 3'); expect(third.textDirection, TextDirection.rtl); expect(third.assertiveness, Assertiveness.polite); @@ -741,6 +745,7 @@ void main() { .setMockDecodedMessageHandler(SystemChannels.accessibility, handleMessage); await SemanticsService.announce( + 0, 'announcement 1', TextDirection.rtl, assertiveness: Assertiveness.assertive, @@ -751,6 +756,7 @@ void main() { { 'type': 'announce', 'data': { + 'viewId': 0, 'message': 'announcement 1', 'textDirection': 0, 'assertiveness': 1, From 6d98c0a681c748f125e50a6f1092980c89607fcd Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Fri, 25 Jul 2025 12:16:47 -0400 Subject: [PATCH 02/23] Various bits of PR feedback --- .../platform/windows/accessibility_plugin.cc | 2 ++ .../src/material/calendar_date_picker.dart | 8 +++--- .../lib/src/material/expansion_tile.dart | 4 +-- .../lib/src/semantics/semantics_event.dart | 5 ++-- .../lib/src/semantics/semantics_service.dart | 25 ++++++++++++++++++- packages/flutter/lib/src/widgets/form.dart | 4 +-- .../semantics/semantics_service_test.dart | 4 +-- .../flutter_test/test/widget_tester_test.dart | 8 +++--- 8 files changed, 43 insertions(+), 17 deletions(-) diff --git a/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc b/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc index 18247a488481c..390e908f06d71 100644 --- a/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc +++ b/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc @@ -64,10 +64,12 @@ void HandleMessage(AccessibilityPlugin* plugin, const EncodableValue& message) { const auto& view_itr = data->find(EncodableValue{kViewIdKey}); if (view_itr == data->end()) { + FML_LOG(ERROR) << "Accessibility message 'viewId' property must be provided."; return; } const auto* view_id = std::get_if(&view_itr->second); if (!view_id) { + FML_LOG(ERROR) << "Accessibility message 'viewId' property must be an int64."; return; } diff --git a/packages/flutter/lib/src/material/calendar_date_picker.dart b/packages/flutter/lib/src/material/calendar_date_picker.dart index 8a7741039ecef..1591845acf563 100644 --- a/packages/flutter/lib/src/material/calendar_date_picker.dart +++ b/packages/flutter/lib/src/material/calendar_date_picker.dart @@ -235,7 +235,7 @@ class _CalendarDatePickerState extends State { _announcedInitialDate = true; final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, _selectedDate); final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : ''; - SemanticsService.announce( + SemanticsService.sendAnnouncement( View.of(context).viewId, '${_localizations.formatFullDate(_selectedDate!)}$semanticLabelSuffix', _textDirection, @@ -265,7 +265,7 @@ class _CalendarDatePickerState extends State { DatePickerMode.day => widget.calendarDelegate.formatMonthYear(selected, _localizations), DatePickerMode.year => widget.calendarDelegate.formatYear(selected.year, _localizations), }; - SemanticsService.announce(View.of(context).viewId, message, _textDirection); + SemanticsService.sendAnnouncement(View.of(context).viewId, message, _textDirection); } }); } @@ -315,7 +315,7 @@ class _CalendarDatePickerState extends State { case TargetPlatform.windows: final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, _selectedDate); final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : ''; - SemanticsService.announce( + SemanticsService.sendAnnouncement( View.of(context).viewId, '${_localizations.selectedDateLabel} ${widget.calendarDelegate.formatFullDate(_selectedDate!, _localizations)}$semanticLabelSuffix', _textDirection, @@ -666,7 +666,7 @@ class _MonthPickerState extends State<_MonthPicker> { // the same day of the month. _focusedDay = _focusableDayForMonth(_currentMonth, _focusedDay!.day); } - SemanticsService.announce( + SemanticsService.sendAnnouncement( View.of(context).viewId, widget.calendarDelegate.formatMonthYear(_currentMonth, _localizations), _textDirection, diff --git a/packages/flutter/lib/src/material/expansion_tile.dart b/packages/flutter/lib/src/material/expansion_tile.dart index 5116a96f4c8f9..aa063137b0d63 100644 --- a/packages/flutter/lib/src/material/expansion_tile.dart +++ b/packages/flutter/lib/src/material/expansion_tile.dart @@ -538,12 +538,12 @@ class _ExpansionTileState extends State { // semantic announcements on iOS. https://github.com/flutter/flutter/issues/122101. _timer?.cancel(); _timer = Timer(const Duration(seconds: 1), () { - SemanticsService.announce(View.of(context).viewId, stateHint, textDirection); + SemanticsService.sendAnnouncement(View.of(context).viewId, stateHint, textDirection); _timer?.cancel(); _timer = null; }); } else { - SemanticsService.announce(View.of(context).viewId, stateHint, textDirection); + SemanticsService.sendAnnouncement(View.of(context).viewId, stateHint, textDirection); } widget.onExpansionChanged?.call(_tileController.isExpanded); } diff --git a/packages/flutter/lib/src/semantics/semantics_event.dart b/packages/flutter/lib/src/semantics/semantics_event.dart index ed24f5c35867e..71839798b53c6 100644 --- a/packages/flutter/lib/src/semantics/semantics_event.dart +++ b/packages/flutter/lib/src/semantics/semantics_event.dart @@ -93,12 +93,13 @@ abstract class SemanticsEvent { /// [1]: https://developer.android.com/reference/android/view/View#announceForAccessibility(java.lang.CharSequence) /// class AnnounceSemanticsEvent extends SemanticsEvent { - /// Constructs an event that triggers an announcement by the platform. + /// Constructs an event that triggers an announcement by the platform + /// on the implicit view const AnnounceSemanticsEvent( - this.viewId, this.message, this.textDirection, { this.assertiveness = Assertiveness.polite, + this.viewId = 0, }) : super('announce'); /// The id of the view that this announcement is on. diff --git a/packages/flutter/lib/src/semantics/semantics_service.dart b/packages/flutter/lib/src/semantics/semantics_service.dart index 176dc814f7847..fc8ee75bbe5e2 100644 --- a/packages/flutter/lib/src/semantics/semantics_service.dart +++ b/packages/flutter/lib/src/semantics/semantics_service.dart @@ -22,7 +22,10 @@ export 'dart:ui' show TextDirection; /// trigger announcements over using this event. abstract final class SemanticsService { /// Sends a semantic announcement. + /// + /// This method is deprecated. Prefer using [sendAnnouncement] instead. /// + /// {@template flutter.semantics.service.announce} /// This should be used for announcement that are not seamlessly announced by /// the system as a result of a UI state change. /// @@ -43,18 +46,38 @@ abstract final class SemanticsService { /// trigger announcements. /// /// [1]: https://developer.android.com/reference/android/view/View#announceForAccessibility(java.lang.CharSequence) + /// {@endtemplate} /// + @Deprecated('Use sendAnnouncement instead.') static Future announce( + String message, + TextDirection textDirection, { + Assertiveness assertiveness = Assertiveness.polite, + }) async { + final AnnounceSemanticsEvent event = AnnounceSemanticsEvent( + message, + textDirection, + assertiveness: assertiveness, + ); + await SystemChannels.accessibility.send(event.toMap()); + } + + + /// Sends a semantic announcement for a particular view. + /// + /// {@macro flutter.semantics.service.announce} + /// + static Future sendAnnouncement( int viewId, String message, TextDirection textDirection, { Assertiveness assertiveness = Assertiveness.polite, }) async { final AnnounceSemanticsEvent event = AnnounceSemanticsEvent( - viewId, message, textDirection, assertiveness: assertiveness, + viewId: viewId ); await SystemChannels.accessibility.send(event.toMap()); } diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart index 657b8aba10779..bbe191fd75d3d 100644 --- a/packages/flutter/lib/src/widgets/form.dart +++ b/packages/flutter/lib/src/widgets/form.dart @@ -384,7 +384,7 @@ class FormState extends State { unawaited( Future(() async { await Future.delayed(_kIOSAnnouncementDelayDuration); - SemanticsService.announce( + SemanticsService.sendAnnouncement( viewId, errorMessage, directionality, @@ -393,7 +393,7 @@ class FormState extends State { }), ); } else { - SemanticsService.announce( + SemanticsService.sendAnnouncement( viewId, errorMessage, directionality, diff --git a/packages/flutter/test/semantics/semantics_service_test.dart b/packages/flutter/test/semantics/semantics_service_test.dart index bae5cecd4124b..9c8989cb66183 100644 --- a/packages/flutter/test/semantics/semantics_service_test.dart +++ b/packages/flutter/test/semantics/semantics_service_test.dart @@ -20,8 +20,8 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockDecodedMessageHandler(SystemChannels.accessibility, handleMessage); - await SemanticsService.announce(1, 'announcement 1', TextDirection.ltr); - await SemanticsService.announce( + await SemanticsService.sendAnnouncement(1, 'announcement 1', TextDirection.ltr); + await SemanticsService.sendAnnouncement( 2, 'announcement 2', TextDirection.rtl, diff --git a/packages/flutter_test/test/widget_tester_test.dart b/packages/flutter_test/test/widget_tester_test.dart index d6684d50e87db..ab96a34ba6568 100644 --- a/packages/flutter_test/test/widget_tester_test.dart +++ b/packages/flutter_test/test/widget_tester_test.dart @@ -701,14 +701,14 @@ void main() { isFalse, ); - await SemanticsService.announce(0, 'announcement 1', TextDirection.ltr); - await SemanticsService.announce( + await SemanticsService.sendAnnouncement(0, 'announcement 1', TextDirection.ltr); + await SemanticsService.sendAnnouncement( 0, 'announcement 2', TextDirection.rtl, assertiveness: Assertiveness.assertive, ); - await SemanticsService.announce(0, 'announcement 3', TextDirection.rtl); + await SemanticsService.sendAnnouncement(0, 'announcement 3', TextDirection.rtl); final List list = tester.takeAnnouncements(); expect(list, hasLength(3)); @@ -744,7 +744,7 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockDecodedMessageHandler(SystemChannels.accessibility, handleMessage); - await SemanticsService.announce( + await SemanticsService.sendAnnouncement( 0, 'announcement 1', TextDirection.rtl, From 0de9a8ebf77f76e1394ae16127b7006b9615d446 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Fri, 25 Jul 2025 12:17:49 -0400 Subject: [PATCH 03/23] Missed a spot! --- dev/integration_tests/new_gallery/lib/pages/backdrop.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/integration_tests/new_gallery/lib/pages/backdrop.dart b/dev/integration_tests/new_gallery/lib/pages/backdrop.dart index a268b0c342a0f..c7133434711ea 100644 --- a/dev/integration_tests/new_gallery/lib/pages/backdrop.dart +++ b/dev/integration_tests/new_gallery/lib/pages/backdrop.dart @@ -264,7 +264,7 @@ class _SettingsIcon extends AnimatedWidget { child: InkWell( onTap: () { toggleSettings(); - SemanticsService.announce( + SemanticsService.sendAnnouncement( View.of(context).viewId, _settingsSemanticLabel(isSettingsOpenNotifier.value, context), GalleryOptions.of(context).resolvedTextDirection()!, From 6e9a70db040498cc3c553851a498a9909e46a7a2 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Fri, 25 Jul 2025 12:54:22 -0400 Subject: [PATCH 04/23] Formatting --- .../flutter/shell/platform/windows/accessibility_plugin.cc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc b/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc index 390e908f06d71..02f8baec24686 100644 --- a/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc +++ b/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc @@ -64,12 +64,14 @@ void HandleMessage(AccessibilityPlugin* plugin, const EncodableValue& message) { const auto& view_itr = data->find(EncodableValue{kViewIdKey}); if (view_itr == data->end()) { - FML_LOG(ERROR) << "Accessibility message 'viewId' property must be provided."; + FML_LOG(ERROR) + << "Accessibility message 'viewId' property must be provided."; return; } const auto* view_id = std::get_if(&view_itr->second); if (!view_id) { - FML_LOG(ERROR) << "Accessibility message 'viewId' property must be an int64."; + FML_LOG(ERROR) + << "Accessibility message 'viewId' property must be an int64."; return; } From d62d1eb3b05b260902fc6394cd3b92a6e081372b Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Fri, 25 Jul 2025 14:21:18 -0400 Subject: [PATCH 05/23] Remove erroneous viewId accessor --- packages/flutter_test/test/widget_tester_test.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/flutter_test/test/widget_tester_test.dart b/packages/flutter_test/test/widget_tester_test.dart index ab96a34ba6568..af1fbe29beac4 100644 --- a/packages/flutter_test/test/widget_tester_test.dart +++ b/packages/flutter_test/test/widget_tester_test.dart @@ -713,18 +713,15 @@ void main() { final List list = tester.takeAnnouncements(); expect(list, hasLength(3)); final CapturedAccessibilityAnnouncement first = list[0]; - expect(first.viewId, 0); expect(first.message, 'announcement 1'); expect(first.textDirection, TextDirection.ltr); final CapturedAccessibilityAnnouncement second = list[1]; - expect(second.viewId, 0); expect(second.message, 'announcement 2'); expect(second.textDirection, TextDirection.rtl); expect(second.assertiveness, Assertiveness.assertive); final CapturedAccessibilityAnnouncement third = list[2]; - expect(third.viewId, 0); expect(third.message, 'announcement 3'); expect(third.textDirection, TextDirection.rtl); expect(third.assertiveness, Assertiveness.polite); From 118d90a70c09703b047449b862a5a93173be64df Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Fri, 25 Jul 2025 14:29:39 -0400 Subject: [PATCH 06/23] PR feedback --- .../platform/windows/accessibility_plugin.cc | 18 ++++++++---------- .../lib/src/semantics/semantics_event.dart | 4 ++-- .../lib/src/semantics/semantics_service.dart | 8 ++++++-- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc b/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc index 02f8baec24686..bf263adfdf748 100644 --- a/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc +++ b/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc @@ -62,20 +62,18 @@ void HandleMessage(AccessibilityPlugin* plugin, const EncodableValue& message) { return; } + FlutterViewId view_id = kImplicitViewId; const auto& view_itr = data->find(EncodableValue{kViewIdKey}); - if (view_itr == data->end()) { - FML_LOG(ERROR) - << "Accessibility message 'viewId' property must be provided."; - return; - } - const auto* view_id = std::get_if(&view_itr->second); - if (!view_id) { - FML_LOG(ERROR) - << "Accessibility message 'viewId' property must be an int64."; + if (view_itr != data->end()) { + const auto* view_id_val = std::get_if(&view_itr->second); + if (view_id_val) { + view_id = *view_id_val; + return; + } return; } - plugin->Announce(*view_id, *message); + plugin->Announce(view_id, *message); } else { FML_LOG(WARNING) << "Accessibility message type '" << *type << "' is not supported."; diff --git a/packages/flutter/lib/src/semantics/semantics_event.dart b/packages/flutter/lib/src/semantics/semantics_event.dart index 71839798b53c6..e443783daeae5 100644 --- a/packages/flutter/lib/src/semantics/semantics_event.dart +++ b/packages/flutter/lib/src/semantics/semantics_event.dart @@ -99,11 +99,11 @@ class AnnounceSemanticsEvent extends SemanticsEvent { this.message, this.textDirection, { this.assertiveness = Assertiveness.polite, - this.viewId = 0, + this.viewId = null, }) : super('announce'); /// The id of the view that this announcement is on. - final int viewId; + final int? viewId; /// The message to announce. final String message; diff --git a/packages/flutter/lib/src/semantics/semantics_service.dart b/packages/flutter/lib/src/semantics/semantics_service.dart index fc8ee75bbe5e2..b3cead4e1a0e4 100644 --- a/packages/flutter/lib/src/semantics/semantics_service.dart +++ b/packages/flutter/lib/src/semantics/semantics_service.dart @@ -22,7 +22,7 @@ export 'dart:ui' show TextDirection; /// trigger announcements over using this event. abstract final class SemanticsService { /// Sends a semantic announcement. - /// + /// /// This method is deprecated. Prefer using [sendAnnouncement] instead. /// /// {@template flutter.semantics.service.announce} @@ -48,7 +48,11 @@ abstract final class SemanticsService { /// [1]: https://developer.android.com/reference/android/view/View#announceForAccessibility(java.lang.CharSequence) /// {@endtemplate} /// - @Deprecated('Use sendAnnouncement instead.') + @Deprecated( + 'Use sendAnnouncement instead. ' + 'This API is incompatible with multiple windows. ' + 'This feature was deprecated after 3.35.0-0.1.pre.' + ) static Future announce( String message, TextDirection textDirection, { From d1d01efdead110882901be31568419e5c0053422 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Fri, 25 Jul 2025 14:33:50 -0400 Subject: [PATCH 07/23] Remove erroneous return statement --- .../src/flutter/shell/platform/windows/accessibility_plugin.cc | 2 -- 1 file changed, 2 deletions(-) diff --git a/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc b/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc index bf263adfdf748..9459962760a6a 100644 --- a/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc +++ b/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc @@ -68,9 +68,7 @@ void HandleMessage(AccessibilityPlugin* plugin, const EncodableValue& message) { const auto* view_id_val = std::get_if(&view_itr->second); if (view_id_val) { view_id = *view_id_val; - return; } - return; } plugin->Announce(view_id, *message); From d22a5fd12b60d1187041859c60a3360385bd3674 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Mon, 28 Jul 2025 10:00:54 -0400 Subject: [PATCH 08/23] Minor nit on comment spacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com> --- packages/flutter/lib/src/semantics/semantics_service.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/flutter/lib/src/semantics/semantics_service.dart b/packages/flutter/lib/src/semantics/semantics_service.dart index b3cead4e1a0e4..71af19408a0b6 100644 --- a/packages/flutter/lib/src/semantics/semantics_service.dart +++ b/packages/flutter/lib/src/semantics/semantics_service.dart @@ -70,7 +70,6 @@ abstract final class SemanticsService { /// Sends a semantic announcement for a particular view. /// /// {@macro flutter.semantics.service.announce} - /// static Future sendAnnouncement( int viewId, String message, From 4ec6112a50d3547c51371ea99833e8df157dc8c3 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Mon, 4 Aug 2025 14:07:54 -0400 Subject: [PATCH 09/23] Refactoring SemanticsService.announce so that it defaults to sending the implicit viewId if none is provided --- .../new_gallery/lib/pages/backdrop.dart | 4 +-- .../src/material/calendar_date_picker.dart | 14 ++++---- .../lib/src/material/expansion_tile.dart | 4 +-- .../lib/src/semantics/semantics_service.dart | 35 ++++--------------- packages/flutter/lib/src/widgets/form.dart | 18 +++++----- .../semantics/semantics_service_test.dart | 6 ++-- .../flutter_test/test/widget_tester_test.dart | 12 +++---- 7 files changed, 35 insertions(+), 58 deletions(-) diff --git a/dev/integration_tests/new_gallery/lib/pages/backdrop.dart b/dev/integration_tests/new_gallery/lib/pages/backdrop.dart index c7133434711ea..ab28c1253e0c9 100644 --- a/dev/integration_tests/new_gallery/lib/pages/backdrop.dart +++ b/dev/integration_tests/new_gallery/lib/pages/backdrop.dart @@ -264,10 +264,10 @@ class _SettingsIcon extends AnimatedWidget { child: InkWell( onTap: () { toggleSettings(); - SemanticsService.sendAnnouncement( - View.of(context).viewId, + SemanticsService.announce( _settingsSemanticLabel(isSettingsOpenNotifier.value, context), GalleryOptions.of(context).resolvedTextDirection()!, + viewId: View.maybeOf(context)?.viewId, ); }, child: Padding( diff --git a/packages/flutter/lib/src/material/calendar_date_picker.dart b/packages/flutter/lib/src/material/calendar_date_picker.dart index 1591845acf563..6e7c8424476c0 100644 --- a/packages/flutter/lib/src/material/calendar_date_picker.dart +++ b/packages/flutter/lib/src/material/calendar_date_picker.dart @@ -235,10 +235,10 @@ class _CalendarDatePickerState extends State { _announcedInitialDate = true; final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, _selectedDate); final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : ''; - SemanticsService.sendAnnouncement( - View.of(context).viewId, + SemanticsService.announce( '${_localizations.formatFullDate(_selectedDate!)}$semanticLabelSuffix', _textDirection, + viewId: View.maybeOf(context)?.viewId, ); } } @@ -265,7 +265,7 @@ class _CalendarDatePickerState extends State { DatePickerMode.day => widget.calendarDelegate.formatMonthYear(selected, _localizations), DatePickerMode.year => widget.calendarDelegate.formatYear(selected.year, _localizations), }; - SemanticsService.sendAnnouncement(View.of(context).viewId, message, _textDirection); + SemanticsService.announce(message, _textDirection, viewId: View.maybeOf(context)?.viewId); } }); } @@ -315,10 +315,10 @@ class _CalendarDatePickerState extends State { case TargetPlatform.windows: final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, _selectedDate); final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : ''; - SemanticsService.sendAnnouncement( - View.of(context).viewId, + SemanticsService.announce( '${_localizations.selectedDateLabel} ${widget.calendarDelegate.formatFullDate(_selectedDate!, _localizations)}$semanticLabelSuffix', _textDirection, + viewId: View.maybeOf(context)?.viewId, ); case TargetPlatform.android: case TargetPlatform.iOS: @@ -666,10 +666,10 @@ class _MonthPickerState extends State<_MonthPicker> { // the same day of the month. _focusedDay = _focusableDayForMonth(_currentMonth, _focusedDay!.day); } - SemanticsService.sendAnnouncement( - View.of(context).viewId, + SemanticsService.announce( widget.calendarDelegate.formatMonthYear(_currentMonth, _localizations), _textDirection, + viewId: View.maybeOf(context)?.viewId, ); } }); diff --git a/packages/flutter/lib/src/material/expansion_tile.dart b/packages/flutter/lib/src/material/expansion_tile.dart index aa063137b0d63..d0436222591fe 100644 --- a/packages/flutter/lib/src/material/expansion_tile.dart +++ b/packages/flutter/lib/src/material/expansion_tile.dart @@ -538,12 +538,12 @@ class _ExpansionTileState extends State { // semantic announcements on iOS. https://github.com/flutter/flutter/issues/122101. _timer?.cancel(); _timer = Timer(const Duration(seconds: 1), () { - SemanticsService.sendAnnouncement(View.of(context).viewId, stateHint, textDirection); + SemanticsService.announce(stateHint, textDirection, viewId: View.maybeOf(context)?.viewId); _timer?.cancel(); _timer = null; }); } else { - SemanticsService.sendAnnouncement(View.of(context).viewId, stateHint, textDirection); + SemanticsService.announce(stateHint, textDirection, viewId: View.maybeOf(context)?.viewId); } widget.onExpansionChanged?.call(_tileController.isExpanded); } diff --git a/packages/flutter/lib/src/semantics/semantics_service.dart b/packages/flutter/lib/src/semantics/semantics_service.dart index 71af19408a0b6..657e88376fd40 100644 --- a/packages/flutter/lib/src/semantics/semantics_service.dart +++ b/packages/flutter/lib/src/semantics/semantics_service.dart @@ -5,7 +5,7 @@ /// @docImport 'package:flutter/widgets.dart'; library; -import 'dart:ui' show TextDirection; +import 'dart:ui' show PlatformDispatcher, TextDirection; import 'package:flutter/services.dart' show SystemChannels; @@ -23,9 +23,6 @@ export 'dart:ui' show TextDirection; abstract final class SemanticsService { /// Sends a semantic announcement. /// - /// This method is deprecated. Prefer using [sendAnnouncement] instead. - /// - /// {@template flutter.semantics.service.announce} /// This should be used for announcement that are not seamlessly announced by /// the system as a result of a UI state change. /// @@ -36,6 +33,9 @@ abstract final class SemanticsService { /// Currently, this is only supported by the web engine and has no effect on /// other platforms. The default mode is [Assertiveness.polite]. /// + /// The [viewId] is the ID of the view that the announcement is associated with. + /// If not provided, it defaults to the implicit view ID of the current platform. + /// /// Not all platforms support announcements. Check to see if it is supported using /// [MediaQuery.supportsAnnounceOf] before calling this method. /// @@ -46,41 +46,18 @@ abstract final class SemanticsService { /// trigger announcements. /// /// [1]: https://developer.android.com/reference/android/view/View#announceForAccessibility(java.lang.CharSequence) - /// {@endtemplate} /// - @Deprecated( - 'Use sendAnnouncement instead. ' - 'This API is incompatible with multiple windows. ' - 'This feature was deprecated after 3.35.0-0.1.pre.' - ) static Future announce( String message, TextDirection textDirection, { Assertiveness assertiveness = Assertiveness.polite, + int? viewId, }) async { final AnnounceSemanticsEvent event = AnnounceSemanticsEvent( message, textDirection, assertiveness: assertiveness, - ); - await SystemChannels.accessibility.send(event.toMap()); - } - - - /// Sends a semantic announcement for a particular view. - /// - /// {@macro flutter.semantics.service.announce} - static Future sendAnnouncement( - int viewId, - String message, - TextDirection textDirection, { - Assertiveness assertiveness = Assertiveness.polite, - }) async { - final AnnounceSemanticsEvent event = AnnounceSemanticsEvent( - message, - textDirection, - assertiveness: assertiveness, - viewId: viewId + viewId: viewId ?? PlatformDispatcher.instance.implicitView?.viewId, ); await SystemChannels.accessibility.send(event.toMap()); } diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart index bbe191fd75d3d..30c48a9b950d2 100644 --- a/packages/flutter/lib/src/widgets/form.dart +++ b/packages/flutter/lib/src/widgets/form.dart @@ -272,10 +272,10 @@ class FormState extends State { Widget build(BuildContext context) { switch (widget.autovalidateMode) { case AutovalidateMode.always: - _validate(View.of(context).viewId); + _validate(View.maybeOf(context)?.viewId); case AutovalidateMode.onUserInteraction: if (_hasInteractedByUser) { - _validate(View.of(context).viewId); + _validate(View.maybeOf(context)?.viewId); } case AutovalidateMode.onUnfocus: case AutovalidateMode.disabled: @@ -336,7 +336,7 @@ class FormState extends State { bool validate() { _hasInteractedByUser = true; _forceRebuild(); - return _validate(View.of(context).viewId); + return _validate(View.maybeOf(context)?.viewId); } /// Validates every [FormField] that is a descendant of this [Form], and @@ -353,11 +353,11 @@ class FormState extends State { final Set> invalidFields = >{}; _hasInteractedByUser = true; _forceRebuild(); - _validate(View.of(context).viewId, invalidFields); + _validate(View.maybeOf(context)?.viewId, invalidFields); return invalidFields; } - bool _validate(int viewId, [Set>? invalidFields]) { + bool _validate(int? viewId, [Set>? invalidFields]) { bool hasError = false; String errorMessage = ''; final bool validateOnFocusChange = widget.autovalidateMode == AutovalidateMode.onUnfocus; @@ -384,20 +384,20 @@ class FormState extends State { unawaited( Future(() async { await Future.delayed(_kIOSAnnouncementDelayDuration); - SemanticsService.sendAnnouncement( - viewId, + SemanticsService.announce( errorMessage, directionality, assertiveness: Assertiveness.assertive, + viewId: viewId, ); }), ); } else { - SemanticsService.sendAnnouncement( - viewId, + SemanticsService.announce( errorMessage, directionality, assertiveness: Assertiveness.assertive, + viewId: viewId, ); } } diff --git a/packages/flutter/test/semantics/semantics_service_test.dart b/packages/flutter/test/semantics/semantics_service_test.dart index 9c8989cb66183..4c9db3d3a9c1b 100644 --- a/packages/flutter/test/semantics/semantics_service_test.dart +++ b/packages/flutter/test/semantics/semantics_service_test.dart @@ -20,12 +20,12 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockDecodedMessageHandler(SystemChannels.accessibility, handleMessage); - await SemanticsService.sendAnnouncement(1, 'announcement 1', TextDirection.ltr); - await SemanticsService.sendAnnouncement( - 2, + await SemanticsService.announce('announcement 1', TextDirection.ltr, viewId: 1); + await SemanticsService.announce( 'announcement 2', TextDirection.rtl, assertiveness: Assertiveness.assertive, + viewId: 2, ); expect( log, diff --git a/packages/flutter_test/test/widget_tester_test.dart b/packages/flutter_test/test/widget_tester_test.dart index af1fbe29beac4..3a58564094aac 100644 --- a/packages/flutter_test/test/widget_tester_test.dart +++ b/packages/flutter_test/test/widget_tester_test.dart @@ -701,14 +701,14 @@ void main() { isFalse, ); - await SemanticsService.sendAnnouncement(0, 'announcement 1', TextDirection.ltr); - await SemanticsService.sendAnnouncement( - 0, + await SemanticsService.announce('announcement 1', TextDirection.ltr, viewId: 0); + await SemanticsService.announce( 'announcement 2', TextDirection.rtl, assertiveness: Assertiveness.assertive, + viewId: 0, ); - await SemanticsService.sendAnnouncement(0, 'announcement 3', TextDirection.rtl); + await SemanticsService.announce('announcement 3', TextDirection.rtl, viewId: 0); final List list = tester.takeAnnouncements(); expect(list, hasLength(3)); @@ -741,11 +741,11 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockDecodedMessageHandler(SystemChannels.accessibility, handleMessage); - await SemanticsService.sendAnnouncement( - 0, + await SemanticsService.announce( 'announcement 1', TextDirection.rtl, assertiveness: Assertiveness.assertive, + viewId: 0, ); expect( log, From b2e94ec592a9e6a63403fc2240893eda339c2820 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Mon, 4 Aug 2025 14:09:59 -0400 Subject: [PATCH 10/23] Remove unnecessary null initializer from AnnounceSemanticsEvent --- packages/flutter/lib/src/semantics/semantics_event.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/semantics/semantics_event.dart b/packages/flutter/lib/src/semantics/semantics_event.dart index e443783daeae5..7782e1116d589 100644 --- a/packages/flutter/lib/src/semantics/semantics_event.dart +++ b/packages/flutter/lib/src/semantics/semantics_event.dart @@ -99,7 +99,7 @@ class AnnounceSemanticsEvent extends SemanticsEvent { this.message, this.textDirection, { this.assertiveness = Assertiveness.polite, - this.viewId = null, + this.viewId, }) : super('announce'); /// The id of the view that this announcement is on. From b11f8dcd16252b562eb4fd8e588f52fd9404474f Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Mon, 4 Aug 2025 14:11:31 -0400 Subject: [PATCH 11/23] Updating an erroneous client --- packages/flutter/lib/src/semantics/semantics_event.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/semantics/semantics_event.dart b/packages/flutter/lib/src/semantics/semantics_event.dart index 7782e1116d589..7255cf775a421 100644 --- a/packages/flutter/lib/src/semantics/semantics_event.dart +++ b/packages/flutter/lib/src/semantics/semantics_event.dart @@ -94,7 +94,7 @@ abstract class SemanticsEvent { /// class AnnounceSemanticsEvent extends SemanticsEvent { /// Constructs an event that triggers an announcement by the platform - /// on the implicit view + /// on the provided [viewId]. const AnnounceSemanticsEvent( this.message, this.textDirection, { From eb6030f8dd60e4d5964595bebce07e132e0ee55f Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Fri, 15 Aug 2025 09:47:14 -0400 Subject: [PATCH 12/23] Revert "Updating an erroneous client" This reverts commit b11f8dcd16252b562eb4fd8e588f52fd9404474f. --- packages/flutter/lib/src/semantics/semantics_event.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/semantics/semantics_event.dart b/packages/flutter/lib/src/semantics/semantics_event.dart index 7255cf775a421..7782e1116d589 100644 --- a/packages/flutter/lib/src/semantics/semantics_event.dart +++ b/packages/flutter/lib/src/semantics/semantics_event.dart @@ -94,7 +94,7 @@ abstract class SemanticsEvent { /// class AnnounceSemanticsEvent extends SemanticsEvent { /// Constructs an event that triggers an announcement by the platform - /// on the provided [viewId]. + /// on the implicit view const AnnounceSemanticsEvent( this.message, this.textDirection, { From bd64992292fcd5d338f67f052d00c5c1f03440c6 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Fri, 15 Aug 2025 09:47:31 -0400 Subject: [PATCH 13/23] Revert "Remove unnecessary null initializer from AnnounceSemanticsEvent" This reverts commit b2e94ec592a9e6a63403fc2240893eda339c2820. --- packages/flutter/lib/src/semantics/semantics_event.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/semantics/semantics_event.dart b/packages/flutter/lib/src/semantics/semantics_event.dart index 7782e1116d589..e443783daeae5 100644 --- a/packages/flutter/lib/src/semantics/semantics_event.dart +++ b/packages/flutter/lib/src/semantics/semantics_event.dart @@ -99,7 +99,7 @@ class AnnounceSemanticsEvent extends SemanticsEvent { this.message, this.textDirection, { this.assertiveness = Assertiveness.polite, - this.viewId, + this.viewId = null, }) : super('announce'); /// The id of the view that this announcement is on. From 9411e548e1ee1ead320de0fb346040961f208fdc Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Fri, 15 Aug 2025 09:48:00 -0400 Subject: [PATCH 14/23] Revert "Refactoring SemanticsService.announce so that it defaults to sending the implicit viewId if none is provided" This reverts commit 4ec6112a50d3547c51371ea99833e8df157dc8c3. --- .../new_gallery/lib/pages/backdrop.dart | 4 +-- .../src/material/calendar_date_picker.dart | 14 ++++---- .../lib/src/material/expansion_tile.dart | 4 +-- .../lib/src/semantics/semantics_service.dart | 35 +++++++++++++++---- packages/flutter/lib/src/widgets/form.dart | 18 +++++----- .../semantics/semantics_service_test.dart | 6 ++-- .../flutter_test/test/widget_tester_test.dart | 12 +++---- 7 files changed, 58 insertions(+), 35 deletions(-) diff --git a/dev/integration_tests/new_gallery/lib/pages/backdrop.dart b/dev/integration_tests/new_gallery/lib/pages/backdrop.dart index ab28c1253e0c9..c7133434711ea 100644 --- a/dev/integration_tests/new_gallery/lib/pages/backdrop.dart +++ b/dev/integration_tests/new_gallery/lib/pages/backdrop.dart @@ -264,10 +264,10 @@ class _SettingsIcon extends AnimatedWidget { child: InkWell( onTap: () { toggleSettings(); - SemanticsService.announce( + SemanticsService.sendAnnouncement( + View.of(context).viewId, _settingsSemanticLabel(isSettingsOpenNotifier.value, context), GalleryOptions.of(context).resolvedTextDirection()!, - viewId: View.maybeOf(context)?.viewId, ); }, child: Padding( diff --git a/packages/flutter/lib/src/material/calendar_date_picker.dart b/packages/flutter/lib/src/material/calendar_date_picker.dart index 6e7c8424476c0..1591845acf563 100644 --- a/packages/flutter/lib/src/material/calendar_date_picker.dart +++ b/packages/flutter/lib/src/material/calendar_date_picker.dart @@ -235,10 +235,10 @@ class _CalendarDatePickerState extends State { _announcedInitialDate = true; final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, _selectedDate); final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : ''; - SemanticsService.announce( + SemanticsService.sendAnnouncement( + View.of(context).viewId, '${_localizations.formatFullDate(_selectedDate!)}$semanticLabelSuffix', _textDirection, - viewId: View.maybeOf(context)?.viewId, ); } } @@ -265,7 +265,7 @@ class _CalendarDatePickerState extends State { DatePickerMode.day => widget.calendarDelegate.formatMonthYear(selected, _localizations), DatePickerMode.year => widget.calendarDelegate.formatYear(selected.year, _localizations), }; - SemanticsService.announce(message, _textDirection, viewId: View.maybeOf(context)?.viewId); + SemanticsService.sendAnnouncement(View.of(context).viewId, message, _textDirection); } }); } @@ -315,10 +315,10 @@ class _CalendarDatePickerState extends State { case TargetPlatform.windows: final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, _selectedDate); final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : ''; - SemanticsService.announce( + SemanticsService.sendAnnouncement( + View.of(context).viewId, '${_localizations.selectedDateLabel} ${widget.calendarDelegate.formatFullDate(_selectedDate!, _localizations)}$semanticLabelSuffix', _textDirection, - viewId: View.maybeOf(context)?.viewId, ); case TargetPlatform.android: case TargetPlatform.iOS: @@ -666,10 +666,10 @@ class _MonthPickerState extends State<_MonthPicker> { // the same day of the month. _focusedDay = _focusableDayForMonth(_currentMonth, _focusedDay!.day); } - SemanticsService.announce( + SemanticsService.sendAnnouncement( + View.of(context).viewId, widget.calendarDelegate.formatMonthYear(_currentMonth, _localizations), _textDirection, - viewId: View.maybeOf(context)?.viewId, ); } }); diff --git a/packages/flutter/lib/src/material/expansion_tile.dart b/packages/flutter/lib/src/material/expansion_tile.dart index d0436222591fe..aa063137b0d63 100644 --- a/packages/flutter/lib/src/material/expansion_tile.dart +++ b/packages/flutter/lib/src/material/expansion_tile.dart @@ -538,12 +538,12 @@ class _ExpansionTileState extends State { // semantic announcements on iOS. https://github.com/flutter/flutter/issues/122101. _timer?.cancel(); _timer = Timer(const Duration(seconds: 1), () { - SemanticsService.announce(stateHint, textDirection, viewId: View.maybeOf(context)?.viewId); + SemanticsService.sendAnnouncement(View.of(context).viewId, stateHint, textDirection); _timer?.cancel(); _timer = null; }); } else { - SemanticsService.announce(stateHint, textDirection, viewId: View.maybeOf(context)?.viewId); + SemanticsService.sendAnnouncement(View.of(context).viewId, stateHint, textDirection); } widget.onExpansionChanged?.call(_tileController.isExpanded); } diff --git a/packages/flutter/lib/src/semantics/semantics_service.dart b/packages/flutter/lib/src/semantics/semantics_service.dart index 657e88376fd40..71af19408a0b6 100644 --- a/packages/flutter/lib/src/semantics/semantics_service.dart +++ b/packages/flutter/lib/src/semantics/semantics_service.dart @@ -5,7 +5,7 @@ /// @docImport 'package:flutter/widgets.dart'; library; -import 'dart:ui' show PlatformDispatcher, TextDirection; +import 'dart:ui' show TextDirection; import 'package:flutter/services.dart' show SystemChannels; @@ -23,6 +23,9 @@ export 'dart:ui' show TextDirection; abstract final class SemanticsService { /// Sends a semantic announcement. /// + /// This method is deprecated. Prefer using [sendAnnouncement] instead. + /// + /// {@template flutter.semantics.service.announce} /// This should be used for announcement that are not seamlessly announced by /// the system as a result of a UI state change. /// @@ -33,9 +36,6 @@ abstract final class SemanticsService { /// Currently, this is only supported by the web engine and has no effect on /// other platforms. The default mode is [Assertiveness.polite]. /// - /// The [viewId] is the ID of the view that the announcement is associated with. - /// If not provided, it defaults to the implicit view ID of the current platform. - /// /// Not all platforms support announcements. Check to see if it is supported using /// [MediaQuery.supportsAnnounceOf] before calling this method. /// @@ -46,18 +46,41 @@ abstract final class SemanticsService { /// trigger announcements. /// /// [1]: https://developer.android.com/reference/android/view/View#announceForAccessibility(java.lang.CharSequence) + /// {@endtemplate} /// + @Deprecated( + 'Use sendAnnouncement instead. ' + 'This API is incompatible with multiple windows. ' + 'This feature was deprecated after 3.35.0-0.1.pre.' + ) static Future announce( String message, TextDirection textDirection, { Assertiveness assertiveness = Assertiveness.polite, - int? viewId, }) async { final AnnounceSemanticsEvent event = AnnounceSemanticsEvent( message, textDirection, assertiveness: assertiveness, - viewId: viewId ?? PlatformDispatcher.instance.implicitView?.viewId, + ); + await SystemChannels.accessibility.send(event.toMap()); + } + + + /// Sends a semantic announcement for a particular view. + /// + /// {@macro flutter.semantics.service.announce} + static Future sendAnnouncement( + int viewId, + String message, + TextDirection textDirection, { + Assertiveness assertiveness = Assertiveness.polite, + }) async { + final AnnounceSemanticsEvent event = AnnounceSemanticsEvent( + message, + textDirection, + assertiveness: assertiveness, + viewId: viewId ); await SystemChannels.accessibility.send(event.toMap()); } diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart index 30c48a9b950d2..bbe191fd75d3d 100644 --- a/packages/flutter/lib/src/widgets/form.dart +++ b/packages/flutter/lib/src/widgets/form.dart @@ -272,10 +272,10 @@ class FormState extends State { Widget build(BuildContext context) { switch (widget.autovalidateMode) { case AutovalidateMode.always: - _validate(View.maybeOf(context)?.viewId); + _validate(View.of(context).viewId); case AutovalidateMode.onUserInteraction: if (_hasInteractedByUser) { - _validate(View.maybeOf(context)?.viewId); + _validate(View.of(context).viewId); } case AutovalidateMode.onUnfocus: case AutovalidateMode.disabled: @@ -336,7 +336,7 @@ class FormState extends State { bool validate() { _hasInteractedByUser = true; _forceRebuild(); - return _validate(View.maybeOf(context)?.viewId); + return _validate(View.of(context).viewId); } /// Validates every [FormField] that is a descendant of this [Form], and @@ -353,11 +353,11 @@ class FormState extends State { final Set> invalidFields = >{}; _hasInteractedByUser = true; _forceRebuild(); - _validate(View.maybeOf(context)?.viewId, invalidFields); + _validate(View.of(context).viewId, invalidFields); return invalidFields; } - bool _validate(int? viewId, [Set>? invalidFields]) { + bool _validate(int viewId, [Set>? invalidFields]) { bool hasError = false; String errorMessage = ''; final bool validateOnFocusChange = widget.autovalidateMode == AutovalidateMode.onUnfocus; @@ -384,20 +384,20 @@ class FormState extends State { unawaited( Future(() async { await Future.delayed(_kIOSAnnouncementDelayDuration); - SemanticsService.announce( + SemanticsService.sendAnnouncement( + viewId, errorMessage, directionality, assertiveness: Assertiveness.assertive, - viewId: viewId, ); }), ); } else { - SemanticsService.announce( + SemanticsService.sendAnnouncement( + viewId, errorMessage, directionality, assertiveness: Assertiveness.assertive, - viewId: viewId, ); } } diff --git a/packages/flutter/test/semantics/semantics_service_test.dart b/packages/flutter/test/semantics/semantics_service_test.dart index 4c9db3d3a9c1b..9c8989cb66183 100644 --- a/packages/flutter/test/semantics/semantics_service_test.dart +++ b/packages/flutter/test/semantics/semantics_service_test.dart @@ -20,12 +20,12 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockDecodedMessageHandler(SystemChannels.accessibility, handleMessage); - await SemanticsService.announce('announcement 1', TextDirection.ltr, viewId: 1); - await SemanticsService.announce( + await SemanticsService.sendAnnouncement(1, 'announcement 1', TextDirection.ltr); + await SemanticsService.sendAnnouncement( + 2, 'announcement 2', TextDirection.rtl, assertiveness: Assertiveness.assertive, - viewId: 2, ); expect( log, diff --git a/packages/flutter_test/test/widget_tester_test.dart b/packages/flutter_test/test/widget_tester_test.dart index 3a58564094aac..af1fbe29beac4 100644 --- a/packages/flutter_test/test/widget_tester_test.dart +++ b/packages/flutter_test/test/widget_tester_test.dart @@ -701,14 +701,14 @@ void main() { isFalse, ); - await SemanticsService.announce('announcement 1', TextDirection.ltr, viewId: 0); - await SemanticsService.announce( + await SemanticsService.sendAnnouncement(0, 'announcement 1', TextDirection.ltr); + await SemanticsService.sendAnnouncement( + 0, 'announcement 2', TextDirection.rtl, assertiveness: Assertiveness.assertive, - viewId: 0, ); - await SemanticsService.announce('announcement 3', TextDirection.rtl, viewId: 0); + await SemanticsService.sendAnnouncement(0, 'announcement 3', TextDirection.rtl); final List list = tester.takeAnnouncements(); expect(list, hasLength(3)); @@ -741,11 +741,11 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockDecodedMessageHandler(SystemChannels.accessibility, handleMessage); - await SemanticsService.announce( + await SemanticsService.sendAnnouncement( + 0, 'announcement 1', TextDirection.rtl, assertiveness: Assertiveness.assertive, - viewId: 0, ); expect( log, From 89b7b26ca205cc1dc498135e35e82f3613a83fc8 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Fri, 15 Aug 2025 10:12:53 -0400 Subject: [PATCH 15/23] Update per the current understanding --- .../new_gallery/lib/pages/backdrop.dart | 2 +- .../lib/src/material/calendar_date_picker.dart | 8 ++++---- .../flutter/lib/src/material/expansion_tile.dart | 4 ++-- .../lib/src/semantics/semantics_event.dart | 9 +++++---- .../lib/src/semantics/semantics_service.dart | 9 ++++----- packages/flutter/lib/src/widgets/form.dart | 15 ++++++++------- .../test/semantics/semantics_service_test.dart | 14 +++++++++----- 7 files changed, 33 insertions(+), 28 deletions(-) diff --git a/dev/integration_tests/new_gallery/lib/pages/backdrop.dart b/dev/integration_tests/new_gallery/lib/pages/backdrop.dart index c7133434711ea..f6df47c23aa59 100644 --- a/dev/integration_tests/new_gallery/lib/pages/backdrop.dart +++ b/dev/integration_tests/new_gallery/lib/pages/backdrop.dart @@ -265,7 +265,7 @@ class _SettingsIcon extends AnimatedWidget { onTap: () { toggleSettings(); SemanticsService.sendAnnouncement( - View.of(context).viewId, + View.of(context), _settingsSemanticLabel(isSettingsOpenNotifier.value, context), GalleryOptions.of(context).resolvedTextDirection()!, ); diff --git a/packages/flutter/lib/src/material/calendar_date_picker.dart b/packages/flutter/lib/src/material/calendar_date_picker.dart index 1591845acf563..2bf8e427cf38d 100644 --- a/packages/flutter/lib/src/material/calendar_date_picker.dart +++ b/packages/flutter/lib/src/material/calendar_date_picker.dart @@ -236,7 +236,7 @@ class _CalendarDatePickerState extends State { final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, _selectedDate); final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : ''; SemanticsService.sendAnnouncement( - View.of(context).viewId, + View.of(context), '${_localizations.formatFullDate(_selectedDate!)}$semanticLabelSuffix', _textDirection, ); @@ -265,7 +265,7 @@ class _CalendarDatePickerState extends State { DatePickerMode.day => widget.calendarDelegate.formatMonthYear(selected, _localizations), DatePickerMode.year => widget.calendarDelegate.formatYear(selected.year, _localizations), }; - SemanticsService.sendAnnouncement(View.of(context).viewId, message, _textDirection); + SemanticsService.sendAnnouncement(View.of(context), message, _textDirection); } }); } @@ -316,7 +316,7 @@ class _CalendarDatePickerState extends State { final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, _selectedDate); final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : ''; SemanticsService.sendAnnouncement( - View.of(context).viewId, + View.of(context), '${_localizations.selectedDateLabel} ${widget.calendarDelegate.formatFullDate(_selectedDate!, _localizations)}$semanticLabelSuffix', _textDirection, ); @@ -667,7 +667,7 @@ class _MonthPickerState extends State<_MonthPicker> { _focusedDay = _focusableDayForMonth(_currentMonth, _focusedDay!.day); } SemanticsService.sendAnnouncement( - View.of(context).viewId, + View.of(context), widget.calendarDelegate.formatMonthYear(_currentMonth, _localizations), _textDirection, ); diff --git a/packages/flutter/lib/src/material/expansion_tile.dart b/packages/flutter/lib/src/material/expansion_tile.dart index aa063137b0d63..ea3eeb36f4ca7 100644 --- a/packages/flutter/lib/src/material/expansion_tile.dart +++ b/packages/flutter/lib/src/material/expansion_tile.dart @@ -538,12 +538,12 @@ class _ExpansionTileState extends State { // semantic announcements on iOS. https://github.com/flutter/flutter/issues/122101. _timer?.cancel(); _timer = Timer(const Duration(seconds: 1), () { - SemanticsService.sendAnnouncement(View.of(context).viewId, stateHint, textDirection); + SemanticsService.sendAnnouncement(View.of(context), stateHint, textDirection); _timer?.cancel(); _timer = null; }); } else { - SemanticsService.sendAnnouncement(View.of(context).viewId, stateHint, textDirection); + SemanticsService.sendAnnouncement(View.of(context), stateHint, textDirection); } widget.onExpansionChanged?.call(_tileController.isExpanded); } diff --git a/packages/flutter/lib/src/semantics/semantics_event.dart b/packages/flutter/lib/src/semantics/semantics_event.dart index e443783daeae5..e5eee18edc486 100644 --- a/packages/flutter/lib/src/semantics/semantics_event.dart +++ b/packages/flutter/lib/src/semantics/semantics_event.dart @@ -6,6 +6,7 @@ /// @docImport 'package:flutter/widgets.dart'; library; +import 'dart:ui' show FlutterView; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; @@ -99,11 +100,11 @@ class AnnounceSemanticsEvent extends SemanticsEvent { this.message, this.textDirection, { this.assertiveness = Assertiveness.polite, - this.viewId = null, + this.view = null, }) : super('announce'); - /// The id of the view that this announcement is on. - final int? viewId; + /// The view that this announcement is on. + final FlutterView? view; /// The message to announce. final String message; @@ -122,7 +123,7 @@ class AnnounceSemanticsEvent extends SemanticsEvent { @override Map getDataMap() { return { - 'viewId': viewId, + 'viewId': view?.viewId, 'message': message, 'textDirection': textDirection.index, if (assertiveness != Assertiveness.polite) 'assertiveness': assertiveness.index, diff --git a/packages/flutter/lib/src/semantics/semantics_service.dart b/packages/flutter/lib/src/semantics/semantics_service.dart index 71af19408a0b6..80658df159cf9 100644 --- a/packages/flutter/lib/src/semantics/semantics_service.dart +++ b/packages/flutter/lib/src/semantics/semantics_service.dart @@ -5,7 +5,7 @@ /// @docImport 'package:flutter/widgets.dart'; library; -import 'dart:ui' show TextDirection; +import 'dart:ui' show FlutterView, TextDirection; import 'package:flutter/services.dart' show SystemChannels; @@ -51,7 +51,7 @@ abstract final class SemanticsService { @Deprecated( 'Use sendAnnouncement instead. ' 'This API is incompatible with multiple windows. ' - 'This feature was deprecated after 3.35.0-0.1.pre.' + 'This feature was deprecated after 3.35.0-0.1.pre.', ) static Future announce( String message, @@ -66,12 +66,11 @@ abstract final class SemanticsService { await SystemChannels.accessibility.send(event.toMap()); } - /// Sends a semantic announcement for a particular view. /// /// {@macro flutter.semantics.service.announce} static Future sendAnnouncement( - int viewId, + FlutterView view, String message, TextDirection textDirection, { Assertiveness assertiveness = Assertiveness.polite, @@ -80,7 +79,7 @@ abstract final class SemanticsService { message, textDirection, assertiveness: assertiveness, - viewId: viewId + view: view, ); await SystemChannels.accessibility.send(event.toMap()); } diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart index bbe191fd75d3d..8e5721d3af668 100644 --- a/packages/flutter/lib/src/widgets/form.dart +++ b/packages/flutter/lib/src/widgets/form.dart @@ -7,6 +7,7 @@ library; import 'dart:async'; +import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; @@ -272,10 +273,10 @@ class FormState extends State { Widget build(BuildContext context) { switch (widget.autovalidateMode) { case AutovalidateMode.always: - _validate(View.of(context).viewId); + _validate(View.of(context)); case AutovalidateMode.onUserInteraction: if (_hasInteractedByUser) { - _validate(View.of(context).viewId); + _validate(View.of(context)); } case AutovalidateMode.onUnfocus: case AutovalidateMode.disabled: @@ -336,7 +337,7 @@ class FormState extends State { bool validate() { _hasInteractedByUser = true; _forceRebuild(); - return _validate(View.of(context).viewId); + return _validate(View.of(context)); } /// Validates every [FormField] that is a descendant of this [Form], and @@ -353,11 +354,11 @@ class FormState extends State { final Set> invalidFields = >{}; _hasInteractedByUser = true; _forceRebuild(); - _validate(View.of(context).viewId, invalidFields); + _validate(View.of(context), invalidFields); return invalidFields; } - bool _validate(int viewId, [Set>? invalidFields]) { + bool _validate(FlutterView view, [Set>? invalidFields]) { bool hasError = false; String errorMessage = ''; final bool validateOnFocusChange = widget.autovalidateMode == AutovalidateMode.onUnfocus; @@ -385,7 +386,7 @@ class FormState extends State { Future(() async { await Future.delayed(_kIOSAnnouncementDelayDuration); SemanticsService.sendAnnouncement( - viewId, + view, errorMessage, directionality, assertiveness: Assertiveness.assertive, @@ -394,7 +395,7 @@ class FormState extends State { ); } else { SemanticsService.sendAnnouncement( - viewId, + view, errorMessage, directionality, assertiveness: Assertiveness.assertive, diff --git a/packages/flutter/test/semantics/semantics_service_test.dart b/packages/flutter/test/semantics/semantics_service_test.dart index 9c8989cb66183..b8c1879d05fa8 100644 --- a/packages/flutter/test/semantics/semantics_service_test.dart +++ b/packages/flutter/test/semantics/semantics_service_test.dart @@ -9,7 +9,7 @@ import 'package:flutter_test/flutter_test.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - test('Semantic announcement', () async { + testWidgets('Semantic announcement', (WidgetTester tester) async { final List> log = >[]; Future handleMessage(dynamic mockMessage) async { @@ -20,9 +20,9 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockDecodedMessageHandler(SystemChannels.accessibility, handleMessage); - await SemanticsService.sendAnnouncement(1, 'announcement 1', TextDirection.ltr); + await SemanticsService.sendAnnouncement(tester.view, 'announcement 1', TextDirection.ltr); await SemanticsService.sendAnnouncement( - 2, + tester.view, 'announcement 2', TextDirection.rtl, assertiveness: Assertiveness.assertive, @@ -32,12 +32,16 @@ void main() { equals(>[ { 'type': 'announce', - 'data': {'viewId': 1, 'message': 'announcement 1', 'textDirection': 1}, + 'data': { + 'viewId': tester.view.viewId, + 'message': 'announcement 1', + 'textDirection': 1, + }, }, { 'type': 'announce', 'data': { - 'viewId': 2, + 'viewId': tester.view.viewId, 'message': 'announcement 2', 'textDirection': 0, 'assertiveness': 1, From 97da60bdcfac38c939b3aa9c728c7a1767459559 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Fri, 15 Aug 2025 10:15:40 -0400 Subject: [PATCH 16/23] Update flutter_test tets --- packages/flutter_test/test/widget_tester_test.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/flutter_test/test/widget_tester_test.dart b/packages/flutter_test/test/widget_tester_test.dart index af1fbe29beac4..687aeb82e098c 100644 --- a/packages/flutter_test/test/widget_tester_test.dart +++ b/packages/flutter_test/test/widget_tester_test.dart @@ -701,14 +701,14 @@ void main() { isFalse, ); - await SemanticsService.sendAnnouncement(0, 'announcement 1', TextDirection.ltr); + await SemanticsService.sendAnnouncement(tester.view, 'announcement 1', TextDirection.ltr); await SemanticsService.sendAnnouncement( - 0, + tester.view, 'announcement 2', TextDirection.rtl, assertiveness: Assertiveness.assertive, ); - await SemanticsService.sendAnnouncement(0, 'announcement 3', TextDirection.rtl); + await SemanticsService.sendAnnouncement(tester.view, 'announcement 3', TextDirection.rtl); final List list = tester.takeAnnouncements(); expect(list, hasLength(3)); @@ -730,7 +730,7 @@ void main() { expect(emptyList, []); }); - test('New test API is not breaking existing tests', () async { + testWidgets('New test API is not breaking existing tests', (WidgetTester tester) async { final List> log = >[]; Future handleMessage(dynamic mockMessage) async { @@ -742,7 +742,7 @@ void main() { .setMockDecodedMessageHandler(SystemChannels.accessibility, handleMessage); await SemanticsService.sendAnnouncement( - 0, + tester.view, 'announcement 1', TextDirection.rtl, assertiveness: Assertiveness.assertive, From 99eac775ac6433e5adc3611c0fc70a70a8807925 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Fri, 15 Aug 2025 13:14:20 -0400 Subject: [PATCH 17/23] Pull request feedback --- packages/flutter/lib/src/semantics/semantics_event.dart | 4 ++-- packages/flutter/lib/src/semantics/semantics_service.dart | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/flutter/lib/src/semantics/semantics_event.dart b/packages/flutter/lib/src/semantics/semantics_event.dart index e5eee18edc486..dbdb614348de9 100644 --- a/packages/flutter/lib/src/semantics/semantics_event.dart +++ b/packages/flutter/lib/src/semantics/semantics_event.dart @@ -100,11 +100,11 @@ class AnnounceSemanticsEvent extends SemanticsEvent { this.message, this.textDirection, { this.assertiveness = Assertiveness.polite, - this.view = null, + this.viewId, }) : super('announce'); /// The view that this announcement is on. - final FlutterView? view; + final int? viewId; /// The message to announce. final String message; diff --git a/packages/flutter/lib/src/semantics/semantics_service.dart b/packages/flutter/lib/src/semantics/semantics_service.dart index 80658df159cf9..5b66128750808 100644 --- a/packages/flutter/lib/src/semantics/semantics_service.dart +++ b/packages/flutter/lib/src/semantics/semantics_service.dart @@ -68,6 +68,8 @@ abstract final class SemanticsService { /// Sends a semantic announcement for a particular view. /// + /// Users can use [View.of] to get the current [FlutterView]. + /// /// {@macro flutter.semantics.service.announce} static Future sendAnnouncement( FlutterView view, @@ -79,7 +81,7 @@ abstract final class SemanticsService { message, textDirection, assertiveness: assertiveness, - view: view, + viewId: view.viewId, ); await SystemChannels.accessibility.send(event.toMap()); } From 5e35a7763efca47f4187a7cde817d03586b33a16 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Fri, 15 Aug 2025 13:16:20 -0400 Subject: [PATCH 18/23] Fix Deprecation notice --- packages/flutter/lib/src/semantics/semantics_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/semantics/semantics_service.dart b/packages/flutter/lib/src/semantics/semantics_service.dart index 5b66128750808..9fe2f05b15d01 100644 --- a/packages/flutter/lib/src/semantics/semantics_service.dart +++ b/packages/flutter/lib/src/semantics/semantics_service.dart @@ -51,7 +51,7 @@ abstract final class SemanticsService { @Deprecated( 'Use sendAnnouncement instead. ' 'This API is incompatible with multiple windows. ' - 'This feature was deprecated after 3.35.0-0.1.pre.', + 'This feature was deprecated after v3.35.0-0.1.pre.', ) static Future announce( String message, From b5f5c08c77e20dfc921948d68916502fd7e4da67 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Fri, 15 Aug 2025 14:47:49 -0400 Subject: [PATCH 19/23] Using viewId in semantic event --- packages/flutter/lib/src/semantics/semantics_event.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/semantics/semantics_event.dart b/packages/flutter/lib/src/semantics/semantics_event.dart index dbdb614348de9..0bd7c821eb53c 100644 --- a/packages/flutter/lib/src/semantics/semantics_event.dart +++ b/packages/flutter/lib/src/semantics/semantics_event.dart @@ -123,7 +123,7 @@ class AnnounceSemanticsEvent extends SemanticsEvent { @override Map getDataMap() { return { - 'viewId': view?.viewId, + 'viewId': viewId, 'message': message, 'textDirection': textDirection.index, if (assertiveness != Assertiveness.polite) 'assertiveness': assertiveness.index, From 832464db6e28ad6f713940da7cd8409ac85c232f Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Tue, 19 Aug 2025 09:09:47 -0400 Subject: [PATCH 20/23] Further pull request feedback --- .../flutter/lib/src/semantics/semantics_event.dart | 7 +++---- .../lib/src/semantics/semantics_service.dart | 13 ++++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/flutter/lib/src/semantics/semantics_event.dart b/packages/flutter/lib/src/semantics/semantics_event.dart index 0bd7c821eb53c..c6bd7d5e8a0e4 100644 --- a/packages/flutter/lib/src/semantics/semantics_event.dart +++ b/packages/flutter/lib/src/semantics/semantics_event.dart @@ -6,7 +6,6 @@ /// @docImport 'package:flutter/widgets.dart'; library; -import 'dart:ui' show FlutterView; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; @@ -98,13 +97,13 @@ class AnnounceSemanticsEvent extends SemanticsEvent { /// on the implicit view const AnnounceSemanticsEvent( this.message, - this.textDirection, { + this.textDirection, + this.viewId, { this.assertiveness = Assertiveness.polite, - this.viewId, }) : super('announce'); /// The view that this announcement is on. - final int? viewId; + final int viewId; /// The message to announce. final String message; diff --git a/packages/flutter/lib/src/semantics/semantics_service.dart b/packages/flutter/lib/src/semantics/semantics_service.dart index 9fe2f05b15d01..5eefa5a0f8f70 100644 --- a/packages/flutter/lib/src/semantics/semantics_service.dart +++ b/packages/flutter/lib/src/semantics/semantics_service.dart @@ -5,7 +5,7 @@ /// @docImport 'package:flutter/widgets.dart'; library; -import 'dart:ui' show FlutterView, TextDirection; +import 'dart:ui' show FlutterView, PlatformDispatcher, TextDirection; import 'package:flutter/services.dart' show SystemChannels; @@ -58,9 +58,16 @@ abstract final class SemanticsService { TextDirection textDirection, { Assertiveness assertiveness = Assertiveness.polite, }) async { + final FlutterView? view = PlatformDispatcher.instance.implicitView; + assert( + view != null, + 'SemanticsService.announce is incompatible with multiple windows. ' + 'Use SemanticsService.sendAnnouncement instead.', + ); final AnnounceSemanticsEvent event = AnnounceSemanticsEvent( message, textDirection, + view!.viewId, assertiveness: assertiveness, ); await SystemChannels.accessibility.send(event.toMap()); @@ -68,7 +75,7 @@ abstract final class SemanticsService { /// Sends a semantic announcement for a particular view. /// - /// Users can use [View.of] to get the current [FlutterView]. + /// You can use [View.of] to get the current [FlutterView]. /// /// {@macro flutter.semantics.service.announce} static Future sendAnnouncement( @@ -80,8 +87,8 @@ abstract final class SemanticsService { final AnnounceSemanticsEvent event = AnnounceSemanticsEvent( message, textDirection, + view.viewId, assertiveness: assertiveness, - viewId: view.viewId, ); await SystemChannels.accessibility.send(event.toMap()); } From 0900f691c59c405a188ff15b0cffbb8ec2bca445 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Tue, 19 Aug 2025 09:16:55 -0400 Subject: [PATCH 21/23] Ensuring that the viewId key is present in the announce event --- .../platform/windows/accessibility_plugin.cc | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc b/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc index 9459962760a6a..f0caa4b6d3741 100644 --- a/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc +++ b/engine/src/flutter/shell/platform/windows/accessibility_plugin.cc @@ -62,16 +62,20 @@ void HandleMessage(AccessibilityPlugin* plugin, const EncodableValue& message) { return; } - FlutterViewId view_id = kImplicitViewId; const auto& view_itr = data->find(EncodableValue{kViewIdKey}); - if (view_itr != data->end()) { - const auto* view_id_val = std::get_if(&view_itr->second); - if (view_id_val) { - view_id = *view_id_val; - } + if (view_itr == data->end()) { + FML_LOG(ERROR) << "Announce message 'viewId' property is missing."; + return; + } + + const auto* view_id_val = std::get_if(&view_itr->second); + if (!view_id_val) { + FML_LOG(ERROR) + << "Announce message 'viewId' property must be a FlutterViewId."; + return; } - plugin->Announce(view_id, *message); + plugin->Announce(*view_id_val, *message); } else { FML_LOG(WARNING) << "Accessibility message type '" << *type << "' is not supported."; From cacccdd55ef6b0e373bf356f08ccc151f2b27b7d Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Tue, 19 Aug 2025 13:42:05 -0400 Subject: [PATCH 22/23] Minor comment nit --- packages/flutter/lib/src/semantics/semantics_event.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/semantics/semantics_event.dart b/packages/flutter/lib/src/semantics/semantics_event.dart index c6bd7d5e8a0e4..801dd357ae759 100644 --- a/packages/flutter/lib/src/semantics/semantics_event.dart +++ b/packages/flutter/lib/src/semantics/semantics_event.dart @@ -94,7 +94,7 @@ abstract class SemanticsEvent { /// class AnnounceSemanticsEvent extends SemanticsEvent { /// Constructs an event that triggers an announcement by the platform - /// on the implicit view + /// for the provided view. const AnnounceSemanticsEvent( this.message, this.textDirection, From 3c4be844db9e7a2c52f999f25f559af841ff23b2 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Tue, 19 Aug 2025 14:29:45 -0400 Subject: [PATCH 23/23] Avoid using 'you' in docs --- packages/flutter/lib/src/semantics/semantics_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/semantics/semantics_service.dart b/packages/flutter/lib/src/semantics/semantics_service.dart index 5eefa5a0f8f70..8104ff0fbafed 100644 --- a/packages/flutter/lib/src/semantics/semantics_service.dart +++ b/packages/flutter/lib/src/semantics/semantics_service.dart @@ -75,7 +75,7 @@ abstract final class SemanticsService { /// Sends a semantic announcement for a particular view. /// - /// You can use [View.of] to get the current [FlutterView]. + /// One can use [View.of] to get the current [FlutterView]. /// /// {@macro flutter.semantics.service.announce} static Future sendAnnouncement(