Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
29aa8c9
Update the AccessibilityPlugin::Announce method to account for the view
mattkae Jul 24, 2025
6d98c0a
Various bits of PR feedback
mattkae Jul 25, 2025
0de9a8e
Missed a spot!
mattkae Jul 25, 2025
6e9a70d
Formatting
mattkae Jul 25, 2025
d62d1eb
Remove erroneous viewId accessor
mattkae Jul 25, 2025
118d90a
PR feedback
mattkae Jul 25, 2025
d1d01ef
Remove erroneous return statement
mattkae Jul 25, 2025
d22a5fd
Minor nit on comment spacing
mattkae Jul 28, 2025
a19e5b3
Merge branch 'master' into announce-event-multi-view
mattkae Jul 28, 2025
802bfab
Merge branch 'master' into announce-event-multi-view
mattkae Jul 31, 2025
4ec6112
Refactoring SemanticsService.announce so that it defaults to sending …
mattkae Aug 4, 2025
5f658d9
Merge branch 'master' into announce-event-multi-view
mattkae Aug 4, 2025
b2e94ec
Remove unnecessary null initializer from AnnounceSemanticsEvent
mattkae Aug 4, 2025
b11f8dc
Updating an erroneous client
mattkae Aug 4, 2025
39a0cc9
Merge branch 'announce-event-multi-view' of github.com:canonical/flut…
mattkae Aug 4, 2025
eb6030f
Revert "Updating an erroneous client"
mattkae Aug 15, 2025
bd64992
Revert "Remove unnecessary null initializer from AnnounceSemanticsEvent"
mattkae Aug 15, 2025
9411e54
Revert "Refactoring SemanticsService.announce so that it defaults to …
mattkae Aug 15, 2025
89b7b26
Update per the current understanding
mattkae Aug 15, 2025
97da60b
Update flutter_test tets
mattkae Aug 15, 2025
af94bf8
Merge branch 'master' into announce-event-multi-view
mattkae Aug 15, 2025
99eac77
Pull request feedback
mattkae Aug 15, 2025
06ccc12
Merge branch 'announce-event-multi-view' of github.com:canonical/flut…
mattkae Aug 15, 2025
5e35a77
Fix Deprecation notice
mattkae Aug 15, 2025
b5f5c08
Using viewId in semantic event
mattkae Aug 15, 2025
832464d
Further pull request feedback
mattkae Aug 19, 2025
0900f69
Ensuring that the viewId key is present in the announce event
mattkae Aug 19, 2025
7feec64
Merge branch 'master' into announce-event-multi-view
mattkae Aug 19, 2025
cacccdd
Minor comment nit
mattkae Aug 19, 2025
2385e37
Merge branch 'master' into announce-event-multi-view
mattkae Aug 19, 2025
3c4be84
Avoid using 'you' in docs
mattkae Aug 19, 2025
2989524
Merge branch 'master' into announce-event-multi-view
mattkae Aug 19, 2025
04f3ea0
Merge branch 'master' into announce-event-multi-view
mattkae Aug 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion dev/integration_tests/new_gallery/lib/pages/backdrop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,8 @@ class _SettingsIcon extends AnimatedWidget {
child: InkWell(
onTap: () {
toggleSettings();
SemanticsService.announce(
SemanticsService.sendAnnouncement(
View.of(context).viewId,
_settingsSemanticLabel(isSettingsOpenNotifier.value, context),
GalleryOptions.of(context).resolvedTextDirection()!,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -61,7 +62,20 @@ void HandleMessage(AccessibilityPlugin* plugin, const EncodableValue& message) {
return;
}

plugin->Announce(*message);
const auto& view_itr = data->find(EncodableValue{kViewIdKey});
Comment thread
mattkae marked this conversation as resolved.
if (view_itr == data->end()) {
FML_LOG(ERROR)
<< "Accessibility message 'viewId' property must be provided.";
return;
Comment thread
loic-sharma marked this conversation as resolved.
}
const auto* view_id = std::get_if<FlutterViewId>(&view_itr->second);
if (!view_id) {
FML_LOG(ERROR)
<< "Accessibility message 'viewId' property must be an int64.";
return;
}

plugin->Announce(*view_id, *message);
} else {
FML_LOG(WARNING) << "Accessibility message type '" << *type
<< "' is not supported.";
Expand Down Expand Up @@ -89,14 +103,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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

namespace flutter {

using FlutterViewId = int64_t;
class FlutterWindowsEngine;

// Handles messages on the flutter/accessibility channel.
Expand All @@ -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.
Expand Down
12 changes: 8 additions & 4 deletions engine/src/flutter/shell/platform/windows/fixtures/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ Future<void> 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
Expand All @@ -73,8 +73,12 @@ Future<void> 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"
Expand Down
11 changes: 7 additions & 4 deletions packages/flutter/lib/src/material/calendar_date_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,8 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
_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,
);
Expand Down Expand Up @@ -264,7 +265,7 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
DatePickerMode.day => widget.calendarDelegate.formatMonthYear(selected, _localizations),
DatePickerMode.year => widget.calendarDelegate.formatYear(selected.year, _localizations),
};
SemanticsService.announce(message, _textDirection);
SemanticsService.sendAnnouncement(View.of(context).viewId, message, _textDirection);
}
});
}
Expand Down Expand Up @@ -314,7 +315,8 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
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,
);
Expand Down Expand Up @@ -664,7 +666,8 @@ 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,
);
Expand Down
4 changes: 2 additions & 2 deletions packages/flutter/lib/src/material/expansion_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -538,12 +538,12 @@ class _ExpansionTileState extends State<ExpansionTile> {
// semantic announcements on iOS. https://github.com/flutter/flutter/issues/122101.
_timer?.cancel();
_timer = Timer(const Duration(seconds: 1), () {
SemanticsService.announce(stateHint, textDirection);
SemanticsService.sendAnnouncement(View.of(context).viewId, stateHint, textDirection);
_timer?.cancel();
_timer = null;
});
} else {
SemanticsService.announce(stateHint, textDirection);
SemanticsService.sendAnnouncement(View.of(context).viewId, stateHint, textDirection);
}
widget.onExpansionChanged?.call(_tileController.isExpanded);
}
Expand Down
8 changes: 7 additions & 1 deletion packages/flutter/lib/src/semantics/semantics_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,18 @@ 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment still accurate?

const AnnounceSemanticsEvent(
this.message,
this.textDirection, {
this.assertiveness = Assertiveness.polite,
this.viewId = 0,
Comment thread
mattkae marked this conversation as resolved.
Outdated
}) : super('announce');

/// The id of the view that this announcement is on.
final int viewId;

/// The message to announce.
final String message;

Expand All @@ -117,6 +122,7 @@ class AnnounceSemanticsEvent extends SemanticsEvent {
@override
Map<String, dynamic> getDataMap() {
return <String, dynamic>{
'viewId': viewId,
'message': message,
'textDirection': textDirection.index,
if (assertiveness != Assertiveness.polite) 'assertiveness': assertiveness.index,
Expand Down
25 changes: 25 additions & 0 deletions packages/flutter/lib/src/semantics/semantics_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -43,7 +46,9 @@ abstract final class SemanticsService {
/// trigger announcements.
///
/// [1]: https://developer.android.com/reference/android/view/View#announceForAccessibility(java.lang.CharSequence)
/// {@endtemplate}
///
@Deprecated('Use sendAnnouncement instead.')
Comment thread
mattkae marked this conversation as resolved.
Outdated
static Future<void> announce(
String message,
TextDirection textDirection, {
Expand All @@ -57,6 +62,26 @@ abstract final class SemanticsService {
await SystemChannels.accessibility.send(event.toMap());
}


/// Sends a semantic announcement for a particular view.
///
/// {@macro flutter.semantics.service.announce}
///
Comment thread
mattkae marked this conversation as resolved.
static Future<void> sendAnnouncement(
Comment thread
loic-sharma marked this conversation as resolved.
int viewId,
Comment thread
loic-sharma marked this conversation as resolved.
Outdated
Comment thread
mattkae marked this conversation as resolved.
Outdated
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());
}

/// Sends a semantic announcement of a tooltip.
///
/// Currently only honored on Android. The contents of [message] will be
Expand Down
17 changes: 10 additions & 7 deletions packages/flutter/lib/src/widgets/form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -271,10 +272,10 @@ class FormState extends State<Form> {
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:
Expand Down Expand Up @@ -335,7 +336,7 @@ class FormState extends State<Form> {
bool validate() {
_hasInteractedByUser = true;
_forceRebuild();
return _validate();
return _validate(View.of(context).viewId);
}

/// Validates every [FormField] that is a descendant of this [Form], and
Expand All @@ -352,11 +353,11 @@ class FormState extends State<Form> {
final Set<FormFieldState<Object?>> invalidFields = <FormFieldState<Object?>>{};
_hasInteractedByUser = true;
_forceRebuild();
_validate(invalidFields);
_validate(View.of(context).viewId, invalidFields);
return invalidFields;
}

bool _validate([Set<FormFieldState<Object?>>? invalidFields]) {
bool _validate(int viewId, [Set<FormFieldState<Object?>>? invalidFields]) {
bool hasError = false;
String errorMessage = '';
final bool validateOnFocusChange = widget.autovalidateMode == AutovalidateMode.onUnfocus;
Expand All @@ -383,15 +384,17 @@ class FormState extends State<Form> {
unawaited(
Future<void>(() async {
await Future<void>.delayed(_kIOSAnnouncementDelayDuration);
SemanticsService.announce(
SemanticsService.sendAnnouncement(
viewId,
errorMessage,
directionality,
assertiveness: Assertiveness.assertive,
);
}),
);
} else {
SemanticsService.announce(
SemanticsService.sendAnnouncement(
viewId,
errorMessage,
directionality,
assertiveness: Assertiveness.assertive,
Expand Down
8 changes: 5 additions & 3 deletions packages/flutter/test/semantics/semantics_service_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ void main() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, handleMessage);

await SemanticsService.announce('announcement 1', TextDirection.ltr);
await SemanticsService.announce(
await SemanticsService.sendAnnouncement(1, 'announcement 1', TextDirection.ltr);
await SemanticsService.sendAnnouncement(
2,
'announcement 2',
TextDirection.rtl,
assertiveness: Assertiveness.assertive,
Expand All @@ -31,11 +32,12 @@ void main() {
equals(<Map<String, dynamic>>[
<String, dynamic>{
'type': 'announce',
'data': <String, dynamic>{'message': 'announcement 1', 'textDirection': 1},
'data': <String, dynamic>{'viewId': 1, 'message': 'announcement 1', 'textDirection': 1},
},
<String, dynamic>{
'type': 'announce',
'data': <String, dynamic>{
'viewId': 2,
'message': 'announcement 2',
'textDirection': 0,
'assertiveness': 1,
Expand Down
14 changes: 10 additions & 4 deletions packages/flutter_test/test/widget_tester_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -701,26 +701,30 @@ void main() {
isFalse,
);

await SemanticsService.announce('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('announcement 3', TextDirection.rtl);
await SemanticsService.sendAnnouncement(0, 'announcement 3', TextDirection.rtl);

final List<CapturedAccessibilityAnnouncement> 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);
Expand All @@ -740,7 +744,8 @@ void main() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, handleMessage);

await SemanticsService.announce(
await SemanticsService.sendAnnouncement(
0,
'announcement 1',
TextDirection.rtl,
assertiveness: Assertiveness.assertive,
Expand All @@ -751,6 +756,7 @@ void main() {
<String, dynamic>{
'type': 'announce',
'data': <String, dynamic>{
'viewId': 0,
'message': 'announcement 1',
'textDirection': 0,
'assertiveness': 1,
Expand Down