Skip to content

Commit 884de61

Browse files
authored
adds status and alert roles (#164925)
<!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> fixes flutter/flutter#162287 fixes flutter/flutter#162286 ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent ea4cdcf commit 884de61

File tree

12 files changed

+283
-0
lines changed

12 files changed

+283
-0
lines changed

engine/src/flutter/ci/licenses_golden/licenses_flutter

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42615,6 +42615,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/scene_painting.dart + ../../.
4261542615
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/scene_view.dart + ../../../flutter/LICENSE
4261642616
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics.dart + ../../../flutter/LICENSE
4261742617
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart + ../../../flutter/LICENSE
42618+
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/alert.dart + ../../../flutter/LICENSE
4261842619
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart + ../../../flutter/LICENSE
4261942620
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/expandable.dart + ../../../flutter/LICENSE
4262042621
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/focusable.dart + ../../../flutter/LICENSE
@@ -45583,6 +45584,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/scene_painting.dart
4558345584
FILE: ../../../flutter/lib/web_ui/lib/src/engine/scene_view.dart
4558445585
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics.dart
4558545586
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart
45587+
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/alert.dart
4558645588
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart
4558745589
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/expandable.dart
4558845590
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/focusable.dart

engine/src/flutter/lib/ui/semantics.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,23 @@ enum SemanticsRole {
462462

463463
/// A group of radio buttons.
464464
radioGroup,
465+
466+
/// A component to provide advisory information that is not important to
467+
/// justify an [alert].
468+
///
469+
/// For example, a loading message for a web page.
470+
status,
471+
472+
/// A component to provide important and usually time-sensitive information.
473+
///
474+
/// The alert role should only be used for information that requires the
475+
/// user's immediate attention, for example:
476+
///
477+
/// * An invalid value was entered into a form field.
478+
/// * The user's login session is about to expire.
479+
/// * The connection to the server was lost so local changes will not be
480+
/// saved.
481+
alert,
465482
}
466483

467484
/// A Boolean value that can be associated with a semantics node.

engine/src/flutter/lib/ui/semantics/semantics_node.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ enum class SemanticsRole : int32_t {
9494
kProgressBar = 22,
9595
kHotKey = 23,
9696
kRadioGroup = 24,
97+
kStatus = 25,
98+
kAlert = 26,
9799
};
98100

99101
/// C/C++ representation of `SemanticsFlags` defined in

engine/src/flutter/lib/web_ui/lib/semantics.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,8 @@ enum SemanticsRole {
282282
progressBar,
283283
hotKey,
284284
radioGroup,
285+
status,
286+
alert,
285287
}
286288

287289
// When adding a new StringAttributeType, the classes in these file must be

engine/src/flutter/lib/web_ui/lib/src/engine.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export 'engine/scene_builder.dart';
104104
export 'engine/scene_painting.dart';
105105
export 'engine/scene_view.dart';
106106
export 'engine/semantics/accessibility.dart';
107+
export 'engine/semantics/alert.dart';
107108
export 'engine/semantics/checkable.dart';
108109
export 'engine/semantics/expandable.dart';
109110
export 'engine/semantics/focusable.dart';

engine/src/flutter/lib/web_ui/lib/src/engine/semantics.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
export 'semantics/accessibility.dart';
6+
export 'semantics/alert.dart';
67
export 'semantics/checkable.dart';
78
export 'semantics/expandable.dart';
89
export 'semantics/focusable.dart';
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2013 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+
import 'label_and_value.dart';
6+
import 'semantics.dart';
7+
8+
/// Renders a piece of alert.
9+
///
10+
/// Uses the ARIA role "alert".
11+
///
12+
/// An alert is similar to [SemanticStatus], but with a higher importantness.
13+
/// For example, a form validation error text.
14+
class SemanticAlert extends SemanticRole {
15+
SemanticAlert(SemanticsObject semanticsObject)
16+
: super.withBasics(
17+
EngineSemanticsRole.alert,
18+
semanticsObject,
19+
preferredLabelRepresentation: LabelRepresentation.ariaLabel,
20+
) {
21+
setAriaRole('alert');
22+
}
23+
24+
@override
25+
bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false;
26+
}
27+
28+
/// Renders a piece of status.
29+
///
30+
/// Uses the ARIA role "status".
31+
///
32+
/// A status is typically used for status updates, such as loading messages,
33+
/// which do not justify to be [SemanticAlert]s.
34+
class SemanticStatus extends SemanticRole {
35+
SemanticStatus(SemanticsObject semanticsObject)
36+
: super.withBasics(
37+
EngineSemanticsRole.status,
38+
semanticsObject,
39+
preferredLabelRepresentation: LabelRepresentation.ariaLabel,
40+
) {
41+
setAriaRole('status');
42+
}
43+
44+
@override
45+
bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false;
46+
}

engine/src/flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import '../util.dart';
1919
import '../vector_math.dart';
2020
import '../window.dart';
2121
import 'accessibility.dart';
22+
import 'alert.dart';
2223
import 'checkable.dart';
2324
import 'expandable.dart';
2425
import 'focusable.dart';
@@ -443,6 +444,13 @@ enum EngineSemanticsRole {
443444
/// A cell in a [table] contains header information for a column.
444445
columnHeader,
445446

447+
/// A component provide advisory information that is not import to justify
448+
/// an [alert].
449+
status,
450+
451+
/// A component provide important and usually time-sensitive information.
452+
alert,
453+
446454
/// A role used when a more specific role cannot be assigend to
447455
/// a [SemanticsObject].
448456
///
@@ -1847,6 +1855,10 @@ class SemanticsObject {
18471855
return EngineSemanticsRole.columnHeader;
18481856
case ui.SemanticsRole.radioGroup:
18491857
return EngineSemanticsRole.radioGroup;
1858+
case ui.SemanticsRole.alert:
1859+
return EngineSemanticsRole.alert;
1860+
case ui.SemanticsRole.status:
1861+
return EngineSemanticsRole.status;
18501862
// TODO(chunhtai): implement these roles.
18511863
// https://github.com/flutter/flutter/issues/159741.
18521864
case ui.SemanticsRole.searchBox:
@@ -1919,6 +1931,8 @@ class SemanticsObject {
19191931
EngineSemanticsRole.cell => SemanticCell(this),
19201932
EngineSemanticsRole.row => SemanticRow(this),
19211933
EngineSemanticsRole.columnHeader => SemanticColumnHeader(this),
1934+
EngineSemanticsRole.alert => SemanticAlert(this),
1935+
EngineSemanticsRole.status => SemanticStatus(this),
19221936
EngineSemanticsRole.generic => GenericRole(this),
19231937
};
19241938
}

engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ void runSemanticsTests() {
110110
group('accessibility builder', () {
111111
_testEngineAccessibilityBuilder();
112112
});
113+
group('alert', () {
114+
_testAlerts();
115+
});
113116
group('group', () {
114117
_testGroup();
115118
});
@@ -297,6 +300,50 @@ void _testEngineAccessibilityBuilder() {
297300
});
298301
}
299302

303+
void _testAlerts() {
304+
test('nodes with alert role', () {
305+
semantics()
306+
..debugOverrideTimestampFunction(() => _testTime)
307+
..semanticsEnabled = true;
308+
309+
SemanticsObject pumpSemantics() {
310+
final SemanticsTester tester = SemanticsTester(owner());
311+
tester.updateNode(
312+
id: 0,
313+
role: ui.SemanticsRole.alert,
314+
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
315+
);
316+
tester.apply();
317+
return tester.getSemanticsObject(0);
318+
}
319+
320+
final SemanticsObject object = pumpSemantics();
321+
expect(object.semanticRole?.kind, EngineSemanticsRole.alert);
322+
expect(object.element.getAttribute('role'), 'alert');
323+
});
324+
325+
test('nodes with status role', () {
326+
semantics()
327+
..debugOverrideTimestampFunction(() => _testTime)
328+
..semanticsEnabled = true;
329+
330+
SemanticsObject pumpSemantics() {
331+
final SemanticsTester tester = SemanticsTester(owner());
332+
tester.updateNode(
333+
id: 0,
334+
role: ui.SemanticsRole.status,
335+
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
336+
);
337+
tester.apply();
338+
return tester.getSemanticsObject(0);
339+
}
340+
341+
final SemanticsObject object = pumpSemantics();
342+
expect(object.semanticRole?.kind, EngineSemanticsRole.status);
343+
expect(object.element.getAttribute('role'), 'status');
344+
});
345+
}
346+
300347
void _testEngineSemanticsOwner() {
301348
test('instantiates a singleton', () {
302349
expect(semantics(), same(semantics()));

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ sealed class _DebugSemanticsRoleChecks {
115115
SemanticsRole.row => _semanticsRow,
116116
SemanticsRole.columnHeader => _semanticsColumnHeader,
117117
SemanticsRole.radioGroup => _semanticsRadioGroup,
118+
SemanticsRole.alert => _noLiveRegion,
119+
SemanticsRole.status => _noLiveRegion,
118120
// TODO(chunhtai): add checks when the roles are used in framework.
119121
// https://github.com/flutter/flutter/issues/159741.
120122
SemanticsRole.searchBox => _unimplemented,
@@ -237,6 +239,18 @@ sealed class _DebugSemanticsRoleChecks {
237239
node.visitChildren(validateRadioGroupChildren);
238240
return error;
239241
}
242+
243+
static FlutterError? _noLiveRegion(SemanticsNode node) {
244+
final SemanticsData data = node.getSemanticsData();
245+
if (data.hasFlag(SemanticsFlag.isLiveRegion)) {
246+
return FlutterError(
247+
'Node ${node.id} has role ${data.role} but is also a live region. '
248+
'A node can not have ${data.role} and be live region at the same time. '
249+
'Either remove the role or the live region',
250+
);
251+
}
252+
return null;
253+
}
240254
}
241255

242256
/// A tag for a [SemanticsNode].

packages/flutter/test/widgets/basic_test.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,34 @@ void main() {
421421
expect(data.controlsNodes, <String>{'abc', 'ghi', 'def'});
422422
});
423423

424+
testWidgets('Semantics can set alert rule', (WidgetTester tester) async {
425+
final UniqueKey key = UniqueKey();
426+
await tester.pumpWidget(
427+
MaterialApp(
428+
home: Scaffold(
429+
body: Semantics(key: key, role: SemanticsRole.alert, child: const Placeholder()),
430+
),
431+
),
432+
);
433+
final SemanticsNode node = tester.getSemantics(find.byKey(key));
434+
final SemanticsData data = node.getSemanticsData();
435+
expect(data.role, SemanticsRole.alert);
436+
});
437+
438+
testWidgets('Semantics can set status rule', (WidgetTester tester) async {
439+
final UniqueKey key = UniqueKey();
440+
await tester.pumpWidget(
441+
MaterialApp(
442+
home: Scaffold(
443+
body: Semantics(key: key, role: SemanticsRole.status, child: const Placeholder()),
444+
),
445+
),
446+
);
447+
final SemanticsNode node = tester.getSemantics(find.byKey(key));
448+
final SemanticsData data = node.getSemanticsData();
449+
expect(data.role, SemanticsRole.status);
450+
});
451+
424452
testWidgets('Semantics can merge attributed strings', (WidgetTester tester) async {
425453
final UniqueKey key = UniqueKey();
426454
await tester.pumpWidget(

0 commit comments

Comments
 (0)