1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5
6import 'package:flutter/foundation.dart';
7import 'package:flutter/painting.dart';
8
9export 'dart:ui' show TextDirection;
10
11/// Determines the assertiveness level of the accessibility announcement.
12///
13/// It is used by [AnnounceSemanticsEvent] to determine the priority with which
14/// assistive technology should treat announcements.
15enum Assertiveness {
16 /// The assistive technology will speak changes whenever the user is idle.
17 polite,
18
19 /// The assistive technology will interrupt any announcement that it is
20 /// currently making to notify the user about the change.
21 ///
22 /// It should only be used for time-sensitive/critical notifications.
23 assertive,
24}
25
26/// An event sent by the application to notify interested listeners that
27/// something happened to the user interface (e.g. a view scrolled).
28///
29/// These events are usually interpreted by assistive technologies to give the
30/// user additional clues about the current state of the UI.
31abstract class SemanticsEvent {
32 /// Initializes internal fields.
33 ///
34 /// [type] is a string that identifies this class of [SemanticsEvent]s.
35 const SemanticsEvent(this.type);
36
37 /// The type of this event.
38 ///
39 /// The type is used by the engine to translate this event into the
40 /// appropriate native event (`UIAccessibility*Notification` on iOS and
41 /// `AccessibilityEvent` on Android).
42 final String type;
43
44 /// Converts this event to a Map that can be encoded with
45 /// [StandardMessageCodec].
46 ///
47 /// [nodeId] is the unique identifier of the semantics node associated with
48 /// the event, or null if the event is not associated with a semantics node.
49 Map<String, dynamic> toMap({ int? nodeId }) {
50 final Map<String, dynamic> event = <String, dynamic>{
51 'type': type,
52 'data': getDataMap(),
53 };
54 if (nodeId != null) {
55 event['nodeId'] = nodeId;
56 }
57
58 return event;
59 }
60
61 /// Returns the event's data object.
62 Map<String, dynamic> getDataMap();
63
64 @override
65 String toString() {
66 final List<String> pairs = <String>[];
67 final Map<String, dynamic> dataMap = getDataMap();
68 final List<String> sortedKeys = dataMap.keys.toList()..sort();
69 for (final String key in sortedKeys) {
70 pairs.add('$key: ${dataMap[key]}');
71 }
72 return '${objectRuntimeType(this, 'SemanticsEvent')}(${pairs.join(', ')})';
73 }
74}
75
76/// An event for a semantic announcement.
77///
78/// This should be used for announcement that are not seamlessly announced by
79/// the system as a result of a UI state change.
80///
81/// For example a camera application can use this method to make accessibility
82/// announcements regarding objects in the viewfinder.
83///
84/// When possible, prefer using mechanisms like [Semantics] to implicitly
85/// trigger announcements over using this event.
86class AnnounceSemanticsEvent extends SemanticsEvent {
87
88 /// Constructs an event that triggers an announcement by the platform.
89 const AnnounceSemanticsEvent(this.message, this.textDirection, {this.assertiveness = Assertiveness.polite})
90 : super('announce');
91
92 /// The message to announce.
93 final String message;
94
95 /// Text direction for [message].
96 final TextDirection textDirection;
97
98 /// Determines whether the announcement should interrupt any existing announcement,
99 /// or queue after it.
100 ///
101 /// On the web this option uses the aria-live level to set the assertiveness
102 /// of the announcement. On iOS, Android, Windows, Linux, macOS, and Fuchsia
103 /// this option currently has no effect.
104 final Assertiveness assertiveness;
105
106 @override
107 Map<String, dynamic> getDataMap() {
108 return <String, dynamic> {
109 'message': message,
110 'textDirection': textDirection.index,
111 if (assertiveness != Assertiveness.polite)
112 'assertiveness': assertiveness.index,
113 };
114 }
115}
116
117/// An event for a semantic announcement of a tooltip.
118///
119/// This is only used by Android to announce tooltip values.
120class TooltipSemanticsEvent extends SemanticsEvent {
121 /// Constructs an event that triggers a tooltip announcement by the platform.
122 const TooltipSemanticsEvent(this.message) : super('tooltip');
123
124 /// The text content of the tooltip.
125 final String message;
126
127 @override
128 Map<String, dynamic> getDataMap() {
129 return <String, dynamic>{
130 'message': message,
131 };
132 }
133}
134
135/// An event which triggers long press semantic feedback.
136///
137/// Currently only honored on Android. Triggers a long-press specific sound
138/// when TalkBack is enabled.
139class LongPressSemanticsEvent extends SemanticsEvent {
140 /// Constructs an event that triggers a long-press semantic feedback by the platform.
141 const LongPressSemanticsEvent() : super('longPress');
142
143 @override
144 Map<String, dynamic> getDataMap() => const <String, dynamic>{};
145}
146
147/// An event which triggers tap semantic feedback.
148///
149/// Currently only honored on Android. Triggers a tap specific sound when
150/// TalkBack is enabled.
151class TapSemanticEvent extends SemanticsEvent {
152 /// Constructs an event that triggers a long-press semantic feedback by the platform.
153 const TapSemanticEvent() : super('tap');
154
155 @override
156 Map<String, dynamic> getDataMap() => const <String, dynamic>{};
157}
158
159/// An event to move the accessibility focus.
160///
161/// Using this API is generally not recommended, as it may break a users' expectation of
162/// how a11y focus works and therefore should be just very carefully.
163///
164/// One possible use case:
165/// For example, the currently focused rendering object is replaced by another rendering
166/// object. In general, such design should be avoided if possible. If not, one may want
167/// to refocus the newly added rendering object.
168///
169/// One example that is not recommended:
170/// When a new popup or dropdown opens, moving the focus in these cases may confuse users
171/// and make it less accessible.
172///
173/// {@tool snippet}
174///
175/// The following code snippet shows how one can request focus on a
176/// certain widget.
177///
178/// ```dart
179/// class MyWidget extends StatefulWidget {
180/// const MyWidget({super.key});
181///
182/// @override
183/// State<MyWidget> createState() => _MyWidgetState();
184/// }
185///
186/// class _MyWidgetState extends State<MyWidget> {
187/// final GlobalKey mykey = GlobalKey();
188///
189/// @override
190/// void initState() {
191/// super.initState();
192/// // Using addPostFrameCallback because changing focus need to wait for the widget to finish rendering.
193/// WidgetsBinding.instance.addPostFrameCallback((_) {
194/// mykey.currentContext?.findRenderObject()?.sendSemanticsEvent(const FocusSemanticEvent());
195/// });
196/// }
197///
198/// @override
199/// Widget build(BuildContext context) {
200/// return Scaffold(
201/// appBar: AppBar(
202/// title: const Text('example'),
203/// ),
204/// body: Column(
205/// children: <Widget>[
206/// const Text('Hello World'),
207/// const SizedBox(height: 50),
208/// Text('set focus here', key: mykey),
209/// ],
210/// ),
211/// );
212/// }
213/// }
214/// ```
215/// {@end-tool}
216///
217/// This currently only supports Android and iOS.
218class FocusSemanticEvent extends SemanticsEvent {
219 /// Constructs an event that triggers a focus change by the platform.
220 const FocusSemanticEvent() : super('focus');
221
222 @override
223 Map<String, dynamic> getDataMap() => const <String, dynamic>{};
224}
225