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 https://github.com/flutter/flutter/issues/162287 fixes https://github.com/flutter/flutter/issues/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
This commit is contained in:
parent
ea4cdcf39e
commit
884de61855
@ -42615,6 +42615,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/scene_painting.dart + ../../.
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/scene_view.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/alert.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/expandable.dart + ../../../flutter/LICENSE
|
||||
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
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/scene_view.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/alert.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/expandable.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/focusable.dart
|
||||
|
@ -462,6 +462,23 @@ enum SemanticsRole {
|
||||
|
||||
/// A group of radio buttons.
|
||||
radioGroup,
|
||||
|
||||
/// A component to provide advisory information that is not important to
|
||||
/// justify an [alert].
|
||||
///
|
||||
/// For example, a loading message for a web page.
|
||||
status,
|
||||
|
||||
/// A component to provide important and usually time-sensitive information.
|
||||
///
|
||||
/// The alert role should only be used for information that requires the
|
||||
/// user's immediate attention, for example:
|
||||
///
|
||||
/// * An invalid value was entered into a form field.
|
||||
/// * The user's login session is about to expire.
|
||||
/// * The connection to the server was lost so local changes will not be
|
||||
/// saved.
|
||||
alert,
|
||||
}
|
||||
|
||||
/// A Boolean value that can be associated with a semantics node.
|
||||
|
@ -94,6 +94,8 @@ enum class SemanticsRole : int32_t {
|
||||
kProgressBar = 22,
|
||||
kHotKey = 23,
|
||||
kRadioGroup = 24,
|
||||
kStatus = 25,
|
||||
kAlert = 26,
|
||||
};
|
||||
|
||||
/// C/C++ representation of `SemanticsFlags` defined in
|
||||
|
@ -282,6 +282,8 @@ enum SemanticsRole {
|
||||
progressBar,
|
||||
hotKey,
|
||||
radioGroup,
|
||||
status,
|
||||
alert,
|
||||
}
|
||||
|
||||
// When adding a new StringAttributeType, the classes in these file must be
|
||||
|
@ -104,6 +104,7 @@ export 'engine/scene_builder.dart';
|
||||
export 'engine/scene_painting.dart';
|
||||
export 'engine/scene_view.dart';
|
||||
export 'engine/semantics/accessibility.dart';
|
||||
export 'engine/semantics/alert.dart';
|
||||
export 'engine/semantics/checkable.dart';
|
||||
export 'engine/semantics/expandable.dart';
|
||||
export 'engine/semantics/focusable.dart';
|
||||
|
@ -3,6 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
export 'semantics/accessibility.dart';
|
||||
export 'semantics/alert.dart';
|
||||
export 'semantics/checkable.dart';
|
||||
export 'semantics/expandable.dart';
|
||||
export 'semantics/focusable.dart';
|
||||
|
@ -0,0 +1,46 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'label_and_value.dart';
|
||||
import 'semantics.dart';
|
||||
|
||||
/// Renders a piece of alert.
|
||||
///
|
||||
/// Uses the ARIA role "alert".
|
||||
///
|
||||
/// An alert is similar to [SemanticStatus], but with a higher importantness.
|
||||
/// For example, a form validation error text.
|
||||
class SemanticAlert extends SemanticRole {
|
||||
SemanticAlert(SemanticsObject semanticsObject)
|
||||
: super.withBasics(
|
||||
EngineSemanticsRole.alert,
|
||||
semanticsObject,
|
||||
preferredLabelRepresentation: LabelRepresentation.ariaLabel,
|
||||
) {
|
||||
setAriaRole('alert');
|
||||
}
|
||||
|
||||
@override
|
||||
bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false;
|
||||
}
|
||||
|
||||
/// Renders a piece of status.
|
||||
///
|
||||
/// Uses the ARIA role "status".
|
||||
///
|
||||
/// A status is typically used for status updates, such as loading messages,
|
||||
/// which do not justify to be [SemanticAlert]s.
|
||||
class SemanticStatus extends SemanticRole {
|
||||
SemanticStatus(SemanticsObject semanticsObject)
|
||||
: super.withBasics(
|
||||
EngineSemanticsRole.status,
|
||||
semanticsObject,
|
||||
preferredLabelRepresentation: LabelRepresentation.ariaLabel,
|
||||
) {
|
||||
setAriaRole('status');
|
||||
}
|
||||
|
||||
@override
|
||||
bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false;
|
||||
}
|
@ -19,6 +19,7 @@ import '../util.dart';
|
||||
import '../vector_math.dart';
|
||||
import '../window.dart';
|
||||
import 'accessibility.dart';
|
||||
import 'alert.dart';
|
||||
import 'checkable.dart';
|
||||
import 'expandable.dart';
|
||||
import 'focusable.dart';
|
||||
@ -443,6 +444,13 @@ enum EngineSemanticsRole {
|
||||
/// A cell in a [table] contains header information for a column.
|
||||
columnHeader,
|
||||
|
||||
/// A component provide advisory information that is not import to justify
|
||||
/// an [alert].
|
||||
status,
|
||||
|
||||
/// A component provide important and usually time-sensitive information.
|
||||
alert,
|
||||
|
||||
/// A role used when a more specific role cannot be assigend to
|
||||
/// a [SemanticsObject].
|
||||
///
|
||||
@ -1847,6 +1855,10 @@ class SemanticsObject {
|
||||
return EngineSemanticsRole.columnHeader;
|
||||
case ui.SemanticsRole.radioGroup:
|
||||
return EngineSemanticsRole.radioGroup;
|
||||
case ui.SemanticsRole.alert:
|
||||
return EngineSemanticsRole.alert;
|
||||
case ui.SemanticsRole.status:
|
||||
return EngineSemanticsRole.status;
|
||||
// TODO(chunhtai): implement these roles.
|
||||
// https://github.com/flutter/flutter/issues/159741.
|
||||
case ui.SemanticsRole.searchBox:
|
||||
@ -1919,6 +1931,8 @@ class SemanticsObject {
|
||||
EngineSemanticsRole.cell => SemanticCell(this),
|
||||
EngineSemanticsRole.row => SemanticRow(this),
|
||||
EngineSemanticsRole.columnHeader => SemanticColumnHeader(this),
|
||||
EngineSemanticsRole.alert => SemanticAlert(this),
|
||||
EngineSemanticsRole.status => SemanticStatus(this),
|
||||
EngineSemanticsRole.generic => GenericRole(this),
|
||||
};
|
||||
}
|
||||
|
@ -110,6 +110,9 @@ void runSemanticsTests() {
|
||||
group('accessibility builder', () {
|
||||
_testEngineAccessibilityBuilder();
|
||||
});
|
||||
group('alert', () {
|
||||
_testAlerts();
|
||||
});
|
||||
group('group', () {
|
||||
_testGroup();
|
||||
});
|
||||
@ -297,6 +300,50 @@ void _testEngineAccessibilityBuilder() {
|
||||
});
|
||||
}
|
||||
|
||||
void _testAlerts() {
|
||||
test('nodes with alert role', () {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
SemanticsObject pumpSemantics() {
|
||||
final SemanticsTester tester = SemanticsTester(owner());
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
role: ui.SemanticsRole.alert,
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
return tester.getSemanticsObject(0);
|
||||
}
|
||||
|
||||
final SemanticsObject object = pumpSemantics();
|
||||
expect(object.semanticRole?.kind, EngineSemanticsRole.alert);
|
||||
expect(object.element.getAttribute('role'), 'alert');
|
||||
});
|
||||
|
||||
test('nodes with status role', () {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
SemanticsObject pumpSemantics() {
|
||||
final SemanticsTester tester = SemanticsTester(owner());
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
role: ui.SemanticsRole.status,
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
return tester.getSemanticsObject(0);
|
||||
}
|
||||
|
||||
final SemanticsObject object = pumpSemantics();
|
||||
expect(object.semanticRole?.kind, EngineSemanticsRole.status);
|
||||
expect(object.element.getAttribute('role'), 'status');
|
||||
});
|
||||
}
|
||||
|
||||
void _testEngineSemanticsOwner() {
|
||||
test('instantiates a singleton', () {
|
||||
expect(semantics(), same(semantics()));
|
||||
|
@ -115,6 +115,8 @@ sealed class _DebugSemanticsRoleChecks {
|
||||
SemanticsRole.row => _semanticsRow,
|
||||
SemanticsRole.columnHeader => _semanticsColumnHeader,
|
||||
SemanticsRole.radioGroup => _semanticsRadioGroup,
|
||||
SemanticsRole.alert => _noLiveRegion,
|
||||
SemanticsRole.status => _noLiveRegion,
|
||||
// TODO(chunhtai): add checks when the roles are used in framework.
|
||||
// https://github.com/flutter/flutter/issues/159741.
|
||||
SemanticsRole.searchBox => _unimplemented,
|
||||
@ -237,6 +239,18 @@ sealed class _DebugSemanticsRoleChecks {
|
||||
node.visitChildren(validateRadioGroupChildren);
|
||||
return error;
|
||||
}
|
||||
|
||||
static FlutterError? _noLiveRegion(SemanticsNode node) {
|
||||
final SemanticsData data = node.getSemanticsData();
|
||||
if (data.hasFlag(SemanticsFlag.isLiveRegion)) {
|
||||
return FlutterError(
|
||||
'Node ${node.id} has role ${data.role} but is also a live region. '
|
||||
'A node can not have ${data.role} and be live region at the same time. '
|
||||
'Either remove the role or the live region',
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// A tag for a [SemanticsNode].
|
||||
|
@ -421,6 +421,34 @@ void main() {
|
||||
expect(data.controlsNodes, <String>{'abc', 'ghi', 'def'});
|
||||
});
|
||||
|
||||
testWidgets('Semantics can set alert rule', (WidgetTester tester) async {
|
||||
final UniqueKey key = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Semantics(key: key, role: SemanticsRole.alert, child: const Placeholder()),
|
||||
),
|
||||
),
|
||||
);
|
||||
final SemanticsNode node = tester.getSemantics(find.byKey(key));
|
||||
final SemanticsData data = node.getSemanticsData();
|
||||
expect(data.role, SemanticsRole.alert);
|
||||
});
|
||||
|
||||
testWidgets('Semantics can set status rule', (WidgetTester tester) async {
|
||||
final UniqueKey key = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Semantics(key: key, role: SemanticsRole.status, child: const Placeholder()),
|
||||
),
|
||||
),
|
||||
);
|
||||
final SemanticsNode node = tester.getSemantics(find.byKey(key));
|
||||
final SemanticsData data = node.getSemanticsData();
|
||||
expect(data.role, SemanticsRole.status);
|
||||
});
|
||||
|
||||
testWidgets('Semantics can merge attributed strings', (WidgetTester tester) async {
|
||||
final UniqueKey key = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
|
@ -267,4 +267,113 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('alert and status', () {
|
||||
testWidgets('failure case, alert and live region', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Semantics(
|
||||
role: SemanticsRole.alert,
|
||||
liveRegion: true,
|
||||
child: const SizedBox.square(dimension: 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
final Object? exception = tester.takeException();
|
||||
expect(exception, isFlutterError);
|
||||
final FlutterError error = exception! as FlutterError;
|
||||
expect(
|
||||
error.message,
|
||||
startsWith('Node 1 has role SemanticsRole.alert but is also a live region.'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('failure case, status and live region', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Semantics(
|
||||
role: SemanticsRole.status,
|
||||
liveRegion: true,
|
||||
child: const SizedBox.square(dimension: 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
final Object? exception = tester.takeException();
|
||||
expect(exception, isFlutterError);
|
||||
final FlutterError error = exception! as FlutterError;
|
||||
expect(
|
||||
error.message,
|
||||
startsWith('Node 1 has role SemanticsRole.status but is also a live region.'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('success case', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Semantics(
|
||||
explicitChildNodes: true,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Semantics(role: SemanticsRole.status, child: const SizedBox.square(dimension: 1)),
|
||||
Semantics(role: SemanticsRole.alert, child: const SizedBox.square(dimension: 1)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('success case, radio buttons with labels', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Semantics(
|
||||
role: SemanticsRole.radioGroup,
|
||||
explicitChildNodes: true,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Semantics(
|
||||
label: 'Option A',
|
||||
child: Semantics(
|
||||
checked: false,
|
||||
inMutuallyExclusiveGroup: true,
|
||||
child: const SizedBox.square(dimension: 1),
|
||||
),
|
||||
),
|
||||
Semantics(
|
||||
label: 'Option B',
|
||||
child: Semantics(
|
||||
checked: true,
|
||||
inMutuallyExclusiveGroup: true,
|
||||
child: const SizedBox.square(dimension: 1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('success case, radio group with no checkable children', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Semantics(
|
||||
role: SemanticsRole.radioGroup,
|
||||
explicitChildNodes: true,
|
||||
child: Semantics(toggled: true, child: const SizedBox.square(dimension: 1)),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user