add semantics role and tab (#161260)
fixes https://github.com/flutter/flutter/issues/157134 ## 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
50f7120de5
commit
6b8b57913d
@ -43284,6 +43284,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/route.dart + ../../
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/tabs.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/tappable.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/text_field.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/services.dart + ../../../flutter/LICENSE
|
||||
@ -46226,6 +46227,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/route.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/tabs.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/tappable.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/text_field.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/services.dart
|
||||
|
@ -238,6 +238,59 @@ void sendSemanticsUpdate() {
|
||||
_semanticsUpdate(builder.build());
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void sendSemanticsUpdateWithRole() {
|
||||
final SemanticsUpdateBuilder builder = SemanticsUpdateBuilder();
|
||||
|
||||
final Float64List transform = Float64List(16);
|
||||
final Int32List childrenInTraversalOrder = Int32List(0);
|
||||
final Int32List childrenInHitTestOrder = Int32List(0);
|
||||
final Int32List additionalActions = Int32List(0);
|
||||
// Identity matrix 4x4.
|
||||
transform[0] = 1;
|
||||
transform[5] = 1;
|
||||
transform[10] = 1;
|
||||
builder.updateNode(
|
||||
id: 0,
|
||||
flags: 0,
|
||||
actions: 0,
|
||||
maxValueLength: 0,
|
||||
currentValueLength: 0,
|
||||
textSelectionBase: -1,
|
||||
textSelectionExtent: -1,
|
||||
platformViewId: -1,
|
||||
scrollChildren: 0,
|
||||
scrollIndex: 0,
|
||||
scrollPosition: 0,
|
||||
scrollExtentMax: 0,
|
||||
scrollExtentMin: 0,
|
||||
rect: Rect.fromLTRB(0, 0, 10, 10),
|
||||
elevation: 0,
|
||||
thickness: 0,
|
||||
identifier: "identifier",
|
||||
label: "label",
|
||||
labelAttributes: const <StringAttribute>[],
|
||||
value: "value",
|
||||
valueAttributes: const <StringAttribute>[],
|
||||
increasedValue: "increasedValue",
|
||||
increasedValueAttributes: const <StringAttribute>[],
|
||||
decreasedValue: "decreasedValue",
|
||||
decreasedValueAttributes: const <StringAttribute>[],
|
||||
hint: "hint",
|
||||
hintAttributes: const <StringAttribute>[],
|
||||
tooltip: "tooltip",
|
||||
textDirection: TextDirection.ltr,
|
||||
transform: transform,
|
||||
childrenInTraversalOrder: childrenInTraversalOrder,
|
||||
childrenInHitTestOrder: childrenInHitTestOrder,
|
||||
additionalActions: additionalActions,
|
||||
headingLevel: 0,
|
||||
linkUrl: '',
|
||||
role: SemanticsRole.tab,
|
||||
);
|
||||
_semanticsUpdate(builder.build());
|
||||
}
|
||||
|
||||
@pragma('vm:external-name', 'SemanticsUpdate')
|
||||
external void _semanticsUpdate(SemanticsUpdate update);
|
||||
|
||||
|
@ -339,6 +339,31 @@ class SemanticsAction {
|
||||
String toString() => 'SemanticsAction.$name';
|
||||
}
|
||||
|
||||
/// An enum to describe the role for a semantics node.
|
||||
///
|
||||
/// The roles are translated into native accessibility roles in each platform.
|
||||
enum SemanticsRole {
|
||||
/// Does not represent any role.
|
||||
none,
|
||||
|
||||
/// A tab button.
|
||||
///
|
||||
/// see also:
|
||||
///
|
||||
/// * [tabBar], which is the role for containers of tab buttons.
|
||||
tab,
|
||||
|
||||
/// Contains tab buttons.
|
||||
///
|
||||
/// see also:
|
||||
///
|
||||
/// * [tab], which is the role for tab buttons.
|
||||
tabBar,
|
||||
|
||||
/// The main display for a tab.
|
||||
tabPanel,
|
||||
}
|
||||
|
||||
/// A Boolean value that can be associated with a semantics node.
|
||||
//
|
||||
// When changes are made to this class, the equivalent APIs in
|
||||
@ -960,6 +985,9 @@ abstract class SemanticsUpdateBuilder {
|
||||
/// The `linkUrl` describes the URI that this node links to. If the node is
|
||||
/// not a link, this should be an empty string.
|
||||
///
|
||||
/// The `role` describes the role of this node. Defaults to
|
||||
/// [SemanticsRole.none] if not set.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/heading_role
|
||||
@ -1000,6 +1028,7 @@ abstract class SemanticsUpdateBuilder {
|
||||
required Int32List additionalActions,
|
||||
int headingLevel = 0,
|
||||
String linkUrl = '',
|
||||
SemanticsRole role = SemanticsRole.none,
|
||||
});
|
||||
|
||||
/// Update the custom semantics action associated with the given `id`.
|
||||
@ -1075,6 +1104,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
|
||||
required Int32List additionalActions,
|
||||
int headingLevel = 0,
|
||||
String linkUrl = '',
|
||||
SemanticsRole role = SemanticsRole.none,
|
||||
}) {
|
||||
assert(_matrix4IsValid(transform));
|
||||
assert(
|
||||
@ -1120,6 +1150,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
|
||||
additionalActions,
|
||||
headingLevel,
|
||||
linkUrl,
|
||||
role.index,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1164,6 +1195,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
|
||||
Handle,
|
||||
Int32,
|
||||
Handle,
|
||||
Int32,
|
||||
)
|
||||
>(symbol: 'SemanticsUpdateBuilder::updateNode')
|
||||
external void _updateNode(
|
||||
@ -1205,6 +1237,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
|
||||
Int32List additionalActions,
|
||||
int headingLevel,
|
||||
String linkUrl,
|
||||
int role,
|
||||
);
|
||||
|
||||
@override
|
||||
|
@ -57,6 +57,19 @@ const int kHorizontalScrollSemanticsActions =
|
||||
const int kScrollableSemanticsActions =
|
||||
kVerticalScrollSemanticsActions | kHorizontalScrollSemanticsActions;
|
||||
|
||||
/// C/C++ representation of `SemanticsRole` defined in
|
||||
/// `lib/ui/semantics.dart`.
|
||||
///\warning This must match the `SemanticsRole` enum in
|
||||
/// `lib/ui/semantics.dart`.
|
||||
/// See also:
|
||||
/// - file://./../../../lib/ui/semantics.dart
|
||||
enum class SemanticsRole : int32_t {
|
||||
kNone = 0,
|
||||
kTab = 1,
|
||||
kTabBar = 2,
|
||||
kTabPanel = 3,
|
||||
};
|
||||
|
||||
/// C/C++ representation of `SemanticsFlags` defined in
|
||||
/// `lib/ui/semantics.dart`.
|
||||
///\warning This must match the `SemanticsFlags` enum in
|
||||
@ -148,6 +161,7 @@ struct SemanticsNode {
|
||||
int32_t headingLevel = 0;
|
||||
|
||||
std::string linkUrl;
|
||||
SemanticsRole role;
|
||||
};
|
||||
|
||||
// Contains semantic nodes that need to be updated.
|
||||
|
@ -68,7 +68,8 @@ void SemanticsUpdateBuilder::updateNode(
|
||||
const tonic::Int32List& childrenInHitTestOrder,
|
||||
const tonic::Int32List& localContextActions,
|
||||
int headingLevel,
|
||||
std::string linkUrl) {
|
||||
std::string linkUrl,
|
||||
int role) {
|
||||
FML_CHECK(scrollChildren == 0 ||
|
||||
(scrollChildren > 0 && childrenInHitTestOrder.data()))
|
||||
<< "Semantics update contained scrollChildren but did not have "
|
||||
@ -119,10 +120,11 @@ void SemanticsUpdateBuilder::updateNode(
|
||||
node.customAccessibilityActions = std::vector<int32_t>(
|
||||
localContextActions.data(),
|
||||
localContextActions.data() + localContextActions.num_elements());
|
||||
nodes_[id] = node;
|
||||
|
||||
node.headingLevel = headingLevel;
|
||||
node.linkUrl = std::move(linkUrl);
|
||||
node.role = static_cast<SemanticsRole>(role);
|
||||
|
||||
nodes_[id] = node;
|
||||
}
|
||||
|
||||
void SemanticsUpdateBuilder::updateCustomAction(int id,
|
||||
|
@ -67,7 +67,8 @@ class SemanticsUpdateBuilder
|
||||
const tonic::Int32List& childrenInHitTestOrder,
|
||||
const tonic::Int32List& customAccessibilityActions,
|
||||
int headingLevel,
|
||||
std::string linkUrl);
|
||||
std::string linkUrl,
|
||||
int role);
|
||||
|
||||
void updateCustomAction(int id,
|
||||
std::string label,
|
||||
|
@ -88,5 +88,48 @@ TEST_F(SemanticsUpdateBuilderTest, CanHandleAttributedStrings) {
|
||||
DestroyShell(std::move(shell), task_runners);
|
||||
}
|
||||
|
||||
TEST_F(SemanticsUpdateBuilderTest, CanHandleSemanticsRole) {
|
||||
auto message_latch = std::make_shared<fml::AutoResetWaitableEvent>();
|
||||
|
||||
auto nativeSemanticsUpdate = [message_latch](Dart_NativeArguments args) {
|
||||
auto handle = Dart_GetNativeArgument(args, 0);
|
||||
intptr_t peer = 0;
|
||||
Dart_Handle result = Dart_GetNativeInstanceField(
|
||||
handle, tonic::DartWrappable::kPeerIndex, &peer);
|
||||
ASSERT_FALSE(Dart_IsError(result));
|
||||
SemanticsUpdate* update = reinterpret_cast<SemanticsUpdate*>(peer);
|
||||
SemanticsNodeUpdates nodes = update->takeNodes();
|
||||
ASSERT_EQ(nodes.size(), (size_t)1);
|
||||
auto node = nodes.find(0)->second;
|
||||
// Should match the updateNode in ui_test.dart.
|
||||
ASSERT_EQ(node.role, SemanticsRole::kTab);
|
||||
message_latch->Signal();
|
||||
};
|
||||
|
||||
Settings settings = CreateSettingsForFixture();
|
||||
TaskRunners task_runners("test", // label
|
||||
GetCurrentTaskRunner(), // platform
|
||||
CreateNewThread(), // raster
|
||||
CreateNewThread(), // ui
|
||||
CreateNewThread() // io
|
||||
);
|
||||
|
||||
AddNativeCallback("SemanticsUpdate",
|
||||
CREATE_NATIVE_ENTRY(nativeSemanticsUpdate));
|
||||
|
||||
std::unique_ptr<Shell> shell = CreateShell(settings, task_runners);
|
||||
|
||||
ASSERT_TRUE(shell->IsSetup());
|
||||
auto configuration = RunConfiguration::InferFromSettings(settings);
|
||||
configuration.SetEntrypoint("sendSemanticsUpdateWithRole");
|
||||
|
||||
shell->RunEngine(std::move(configuration), [](auto result) {
|
||||
ASSERT_EQ(result, Engine::RunStatus::Success);
|
||||
});
|
||||
|
||||
message_latch->Wait();
|
||||
DestroyShell(std::move(shell), task_runners);
|
||||
}
|
||||
|
||||
} // namespace testing
|
||||
} // namespace flutter
|
||||
|
@ -255,6 +255,9 @@ class SemanticsFlag {
|
||||
String toString() => 'SemanticsFlag.$name';
|
||||
}
|
||||
|
||||
// Mirrors engine/src/flutter/lib/ui/semantics.dart
|
||||
enum SemanticsRole { none, tab, tabBar, tabPanel }
|
||||
|
||||
// When adding a new StringAttributeType, the classes in these file must be
|
||||
// updated as well.
|
||||
// * engine/src/flutter/lib/ui/semantics.dart
|
||||
@ -341,6 +344,7 @@ class SemanticsUpdateBuilder {
|
||||
required Int32List additionalActions,
|
||||
int headingLevel = 0,
|
||||
String? linkUrl,
|
||||
SemanticsRole role = SemanticsRole.none,
|
||||
}) {
|
||||
if (transform.length != 16) {
|
||||
throw ArgumentError('transform argument must have 16 entries.');
|
||||
@ -382,6 +386,7 @@ class SemanticsUpdateBuilder {
|
||||
platformViewId: platformViewId,
|
||||
headingLevel: headingLevel,
|
||||
linkUrl: linkUrl,
|
||||
role: role,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -159,6 +159,7 @@ export 'engine/semantics/route.dart';
|
||||
export 'engine/semantics/scrollable.dart';
|
||||
export 'engine/semantics/semantics.dart';
|
||||
export 'engine/semantics/semantics_helper.dart';
|
||||
export 'engine/semantics/tabs.dart';
|
||||
export 'engine/semantics/tappable.dart';
|
||||
export 'engine/semantics/text_field.dart';
|
||||
export 'engine/services/buffers.dart';
|
||||
|
@ -16,5 +16,6 @@ export 'semantics/platform_view.dart';
|
||||
export 'semantics/scrollable.dart';
|
||||
export 'semantics/semantics.dart';
|
||||
export 'semantics/semantics_helper.dart';
|
||||
export 'semantics/tabs.dart';
|
||||
export 'semantics/tappable.dart';
|
||||
export 'semantics/text_field.dart';
|
||||
|
@ -55,7 +55,7 @@ class SemanticCheckable extends SemanticRole {
|
||||
SemanticCheckable(SemanticsObject semanticsObject)
|
||||
: _kind = _checkableKindFromSemanticsFlag(semanticsObject),
|
||||
super.withBasics(
|
||||
SemanticRoleKind.checkable,
|
||||
EngineSemanticsRole.checkable,
|
||||
semanticsObject,
|
||||
preferredLabelRepresentation: LabelRepresentation.ariaLabel,
|
||||
) {
|
||||
|
@ -19,7 +19,7 @@ import 'semantics.dart';
|
||||
class SemanticHeader extends SemanticRole {
|
||||
SemanticHeader(SemanticsObject semanticsObject)
|
||||
: super.withBasics(
|
||||
SemanticRoleKind.header,
|
||||
EngineSemanticsRole.header,
|
||||
semanticsObject,
|
||||
|
||||
// Why use sizedSpan?
|
||||
|
@ -10,7 +10,7 @@ import 'semantics.dart';
|
||||
/// level (h1 ... h6).
|
||||
class SemanticHeading extends SemanticRole {
|
||||
SemanticHeading(SemanticsObject semanticsObject)
|
||||
: super.blank(SemanticRoleKind.heading, semanticsObject) {
|
||||
: super.blank(EngineSemanticsRole.heading, semanticsObject) {
|
||||
addFocusManagement();
|
||||
addLiveRegion();
|
||||
addRouteName();
|
||||
|
@ -12,7 +12,7 @@ import 'semantics.dart';
|
||||
/// Screen-readers takes advantage of "aria-label" to describe the visual.
|
||||
class SemanticImage extends SemanticRole {
|
||||
SemanticImage(SemanticsObject semanticsObject)
|
||||
: super.blank(SemanticRoleKind.image, semanticsObject) {
|
||||
: super.blank(EngineSemanticsRole.image, semanticsObject) {
|
||||
// The following behaviors can coexist with images. `LabelAndValue` is
|
||||
// not used because this behavior uses special auxiliary elements to
|
||||
// supply ARIA labels.
|
||||
|
@ -22,7 +22,7 @@ import 'semantics.dart';
|
||||
class SemanticIncrementable extends SemanticRole {
|
||||
SemanticIncrementable(SemanticsObject semanticsObject)
|
||||
: _focusManager = AccessibilityFocusManager(semanticsObject.owner),
|
||||
super.blank(SemanticRoleKind.incrementable, semanticsObject) {
|
||||
super.blank(EngineSemanticsRole.incrementable, semanticsObject) {
|
||||
// The following generic roles can coexist with incrementables. Generic focus
|
||||
// management is not used by this role because the root DOM element is not
|
||||
// the one being focused on, but the internal `<input>` element.
|
||||
|
@ -9,7 +9,7 @@ import '../semantics.dart';
|
||||
class SemanticLink extends SemanticRole {
|
||||
SemanticLink(SemanticsObject semanticsObject)
|
||||
: super.withBasics(
|
||||
SemanticRoleKind.link,
|
||||
EngineSemanticsRole.link,
|
||||
semanticsObject,
|
||||
preferredLabelRepresentation: LabelRepresentation.domText,
|
||||
) {
|
||||
|
@ -23,7 +23,7 @@ import 'semantics.dart';
|
||||
class SemanticPlatformView extends SemanticRole {
|
||||
SemanticPlatformView(SemanticsObject semanticsObject)
|
||||
: super.withBasics(
|
||||
SemanticRoleKind.platformView,
|
||||
EngineSemanticsRole.platformView,
|
||||
semanticsObject,
|
||||
preferredLabelRepresentation: LabelRepresentation.ariaLabel,
|
||||
);
|
||||
|
@ -16,7 +16,7 @@ import '../util.dart';
|
||||
/// of an explicit route label set on the route itself.
|
||||
class SemanticRoute extends SemanticRole {
|
||||
SemanticRoute(SemanticsObject semanticsObject)
|
||||
: super.blank(SemanticRoleKind.route, semanticsObject) {
|
||||
: super.blank(EngineSemanticsRole.route, semanticsObject) {
|
||||
// The following behaviors can coexist with the route. Generic `RouteName`
|
||||
// and `LabelAndValue` are not used by this role because when the route
|
||||
// names its own route an `aria-label` is used instead of
|
||||
@ -158,10 +158,10 @@ class RouteName extends SemanticBehavior {
|
||||
|
||||
void _lookUpNearestAncestorRoute() {
|
||||
SemanticsObject? parent = semanticsObject.parent;
|
||||
while (parent != null && parent.semanticRole?.kind != SemanticRoleKind.route) {
|
||||
while (parent != null && parent.semanticRole?.kind != EngineSemanticsRole.route) {
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (parent != null && parent.semanticRole?.kind == SemanticRoleKind.route) {
|
||||
if (parent != null && parent.semanticRole?.kind == EngineSemanticsRole.route) {
|
||||
_route = parent.semanticRole! as SemanticRoute;
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ import 'package:ui/ui.dart' as ui;
|
||||
class SemanticScrollable extends SemanticRole {
|
||||
SemanticScrollable(SemanticsObject semanticsObject)
|
||||
: super.withBasics(
|
||||
SemanticRoleKind.scrollable,
|
||||
EngineSemanticsRole.scrollable,
|
||||
semanticsObject,
|
||||
preferredLabelRepresentation: LabelRepresentation.ariaLabel,
|
||||
) {
|
||||
|
@ -32,6 +32,7 @@ import 'platform_view.dart';
|
||||
import 'route.dart';
|
||||
import 'scrollable.dart';
|
||||
import 'semantics_helper.dart';
|
||||
import 'tabs.dart';
|
||||
import 'tappable.dart';
|
||||
import 'text_field.dart';
|
||||
|
||||
@ -235,6 +236,7 @@ class SemanticsNodeUpdate {
|
||||
required this.additionalActions,
|
||||
required this.headingLevel,
|
||||
this.linkUrl,
|
||||
required this.role,
|
||||
});
|
||||
|
||||
/// See [ui.SemanticsUpdateBuilder.updateNode].
|
||||
@ -341,13 +343,16 @@ class SemanticsNodeUpdate {
|
||||
|
||||
/// See [ui.SemanticsUpdateBuilder.updateNode].
|
||||
final String? linkUrl;
|
||||
|
||||
/// See [ui.SemanticsUpdateBuilder.updateNode].
|
||||
final ui.SemanticsRole role;
|
||||
}
|
||||
|
||||
/// Identifies [SemanticRole] implementations.
|
||||
///
|
||||
/// Each value corresponds to the most specific role a semantics node plays in
|
||||
/// the semantics tree.
|
||||
enum SemanticRoleKind {
|
||||
enum EngineSemanticsRole {
|
||||
/// Supports incrementing and/or decrementing its value.
|
||||
incrementable,
|
||||
|
||||
@ -402,6 +407,15 @@ enum SemanticRoleKind {
|
||||
/// Denotes a header.
|
||||
header,
|
||||
|
||||
/// An individual tab button.
|
||||
tab,
|
||||
|
||||
/// Contains tab buttons.
|
||||
tabList,
|
||||
|
||||
/// A main content for a tab.
|
||||
tabPanel,
|
||||
|
||||
/// A role used when a more specific role cannot be assigend to
|
||||
/// a [SemanticsObject].
|
||||
///
|
||||
@ -442,7 +456,7 @@ abstract class SemanticRole {
|
||||
late final DomElement element;
|
||||
|
||||
/// The kind of the role that this .
|
||||
final SemanticRoleKind kind;
|
||||
final EngineSemanticsRole kind;
|
||||
|
||||
/// The semantics object managed by this role.
|
||||
final SemanticsObject semanticsObject;
|
||||
@ -678,7 +692,7 @@ abstract class SemanticRole {
|
||||
final class GenericRole extends SemanticRole {
|
||||
GenericRole(SemanticsObject semanticsObject)
|
||||
: super.withBasics(
|
||||
SemanticRoleKind.generic,
|
||||
EngineSemanticsRole.generic,
|
||||
semanticsObject,
|
||||
// Prefer sized span because if this is a leaf it is frequently a Text widget.
|
||||
// But if it turns out to be a container, then LabelAndValue will automatically
|
||||
@ -1232,6 +1246,9 @@ class SemanticsObject {
|
||||
/// Controls the semantics tree that this node participates in.
|
||||
final EngineSemanticsOwner owner;
|
||||
|
||||
/// The role of this node.
|
||||
late ui.SemanticsRole role;
|
||||
|
||||
/// Bitfield showing which fields have been updated but have not yet been
|
||||
/// applied to the DOM.
|
||||
///
|
||||
@ -1518,6 +1535,8 @@ class SemanticsObject {
|
||||
_markLinkUrlDirty();
|
||||
}
|
||||
|
||||
role = update.role;
|
||||
|
||||
// Apply updates to the DOM.
|
||||
_updateRole();
|
||||
|
||||
@ -1714,51 +1733,66 @@ class SemanticsObject {
|
||||
/// semantics flags and actions.
|
||||
SemanticRole? semanticRole;
|
||||
|
||||
SemanticRoleKind _getSemanticRoleKind() {
|
||||
EngineSemanticsRole _getEngineSemanticsRole() {
|
||||
// The most specific role should take precedence.
|
||||
if (isPlatformView) {
|
||||
return SemanticRoleKind.platformView;
|
||||
} else if (isHeading) {
|
||||
return EngineSemanticsRole.platformView;
|
||||
}
|
||||
switch (role) {
|
||||
case ui.SemanticsRole.tab:
|
||||
return EngineSemanticsRole.tab;
|
||||
case ui.SemanticsRole.tabPanel:
|
||||
return EngineSemanticsRole.tabPanel;
|
||||
case ui.SemanticsRole.tabBar:
|
||||
return EngineSemanticsRole.tabList;
|
||||
case ui.SemanticsRole.none:
|
||||
// fallback to checking semantics properties.
|
||||
}
|
||||
|
||||
if (isHeading) {
|
||||
// IMPORTANT: because headings also cover certain kinds of headers, the
|
||||
// `heading` role has precedence over the `header` role.
|
||||
return SemanticRoleKind.heading;
|
||||
return EngineSemanticsRole.heading;
|
||||
} else if (isTextField) {
|
||||
return SemanticRoleKind.textField;
|
||||
return EngineSemanticsRole.textField;
|
||||
} else if (isIncrementable) {
|
||||
return SemanticRoleKind.incrementable;
|
||||
return EngineSemanticsRole.incrementable;
|
||||
} else if (isVisualOnly) {
|
||||
return SemanticRoleKind.image;
|
||||
return EngineSemanticsRole.image;
|
||||
} else if (isCheckable) {
|
||||
return SemanticRoleKind.checkable;
|
||||
return EngineSemanticsRole.checkable;
|
||||
} else if (isButton) {
|
||||
return SemanticRoleKind.button;
|
||||
return EngineSemanticsRole.button;
|
||||
} else if (isScrollContainer) {
|
||||
return SemanticRoleKind.scrollable;
|
||||
return EngineSemanticsRole.scrollable;
|
||||
} else if (scopesRoute) {
|
||||
return SemanticRoleKind.route;
|
||||
return EngineSemanticsRole.route;
|
||||
} else if (isLink) {
|
||||
return SemanticRoleKind.link;
|
||||
return EngineSemanticsRole.link;
|
||||
} else if (isHeader) {
|
||||
return SemanticRoleKind.header;
|
||||
return EngineSemanticsRole.header;
|
||||
} else {
|
||||
return SemanticRoleKind.generic;
|
||||
return EngineSemanticsRole.generic;
|
||||
}
|
||||
}
|
||||
|
||||
SemanticRole _createSemanticRole(SemanticRoleKind role) {
|
||||
SemanticRole _createSemanticRole(EngineSemanticsRole role) {
|
||||
return switch (role) {
|
||||
SemanticRoleKind.textField => SemanticTextField(this),
|
||||
SemanticRoleKind.scrollable => SemanticScrollable(this),
|
||||
SemanticRoleKind.incrementable => SemanticIncrementable(this),
|
||||
SemanticRoleKind.button => SemanticButton(this),
|
||||
SemanticRoleKind.checkable => SemanticCheckable(this),
|
||||
SemanticRoleKind.route => SemanticRoute(this),
|
||||
SemanticRoleKind.image => SemanticImage(this),
|
||||
SemanticRoleKind.platformView => SemanticPlatformView(this),
|
||||
SemanticRoleKind.link => SemanticLink(this),
|
||||
SemanticRoleKind.heading => SemanticHeading(this),
|
||||
SemanticRoleKind.header => SemanticHeader(this),
|
||||
SemanticRoleKind.generic => GenericRole(this),
|
||||
EngineSemanticsRole.textField => SemanticTextField(this),
|
||||
EngineSemanticsRole.scrollable => SemanticScrollable(this),
|
||||
EngineSemanticsRole.incrementable => SemanticIncrementable(this),
|
||||
EngineSemanticsRole.button => SemanticButton(this),
|
||||
EngineSemanticsRole.checkable => SemanticCheckable(this),
|
||||
EngineSemanticsRole.route => SemanticRoute(this),
|
||||
EngineSemanticsRole.image => SemanticImage(this),
|
||||
EngineSemanticsRole.platformView => SemanticPlatformView(this),
|
||||
EngineSemanticsRole.link => SemanticLink(this),
|
||||
EngineSemanticsRole.heading => SemanticHeading(this),
|
||||
EngineSemanticsRole.header => SemanticHeader(this),
|
||||
EngineSemanticsRole.tab => SemanticTab(this),
|
||||
EngineSemanticsRole.tabList => SemanticTabList(this),
|
||||
EngineSemanticsRole.tabPanel => SemanticTabPanel(this),
|
||||
EngineSemanticsRole.generic => GenericRole(this),
|
||||
};
|
||||
}
|
||||
|
||||
@ -1766,7 +1800,7 @@ class SemanticsObject {
|
||||
/// update the DOM.
|
||||
void _updateRole() {
|
||||
SemanticRole? currentSemanticRole = semanticRole;
|
||||
final SemanticRoleKind kind = _getSemanticRoleKind();
|
||||
final EngineSemanticsRole kind = _getEngineSemanticsRole();
|
||||
final DomElement? previousElement = semanticRole?.element;
|
||||
|
||||
if (currentSemanticRole != null) {
|
||||
|
@ -0,0 +1,64 @@
|
||||
// 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';
|
||||
|
||||
/// Indicates an interactive element inside a tablist that, when activated,
|
||||
/// displays its associated tabpanel.
|
||||
///
|
||||
/// Uses aria tab role to convey this semantic information to the element.
|
||||
///
|
||||
/// Screen-readers takes advantage of "aria-label" to describe the visual.
|
||||
class SemanticTab extends SemanticRole {
|
||||
SemanticTab(SemanticsObject semanticsObject)
|
||||
: super.withBasics(
|
||||
EngineSemanticsRole.tab,
|
||||
semanticsObject,
|
||||
preferredLabelRepresentation: LabelRepresentation.ariaLabel,
|
||||
) {
|
||||
setAriaRole('tab');
|
||||
}
|
||||
|
||||
@override
|
||||
bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false;
|
||||
}
|
||||
|
||||
/// Indicates the main display for a tab when activated.
|
||||
///
|
||||
/// Uses aria tabpanel role to convey this semantic information to the element.
|
||||
///
|
||||
/// Screen-readers takes advantage of "aria-label" to describe the visual.
|
||||
class SemanticTabPanel extends SemanticRole {
|
||||
SemanticTabPanel(SemanticsObject semanticsObject)
|
||||
: super.withBasics(
|
||||
EngineSemanticsRole.tabPanel,
|
||||
semanticsObject,
|
||||
preferredLabelRepresentation: LabelRepresentation.ariaLabel,
|
||||
) {
|
||||
setAriaRole('tabpanel');
|
||||
}
|
||||
|
||||
@override
|
||||
bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false;
|
||||
}
|
||||
|
||||
/// Indicates a container that contains multiple tabs.
|
||||
///
|
||||
/// Uses aria tablist role to convey this semantic information to the element.
|
||||
///
|
||||
/// Screen-readers takes advantage of "aria-label" to describe the visual.
|
||||
class SemanticTabList extends SemanticRole {
|
||||
SemanticTabList(SemanticsObject semanticsObject)
|
||||
: super.withBasics(
|
||||
EngineSemanticsRole.tabList,
|
||||
semanticsObject,
|
||||
preferredLabelRepresentation: LabelRepresentation.ariaLabel,
|
||||
) {
|
||||
setAriaRole('tablist');
|
||||
}
|
||||
|
||||
@override
|
||||
bool focusAsRouteDefault() => focusable?.focusAsRouteDefault() ?? false;
|
||||
}
|
@ -9,7 +9,7 @@ import 'package:ui/ui.dart' as ui;
|
||||
class SemanticButton extends SemanticRole {
|
||||
SemanticButton(SemanticsObject semanticsObject)
|
||||
: super.withBasics(
|
||||
SemanticRoleKind.button,
|
||||
EngineSemanticsRole.button,
|
||||
semanticsObject,
|
||||
preferredLabelRepresentation: LabelRepresentation.domText,
|
||||
) {
|
||||
|
@ -196,7 +196,7 @@ class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
/// events even when VoiceOver is enabled.
|
||||
class SemanticTextField extends SemanticRole {
|
||||
SemanticTextField(SemanticsObject semanticsObject)
|
||||
: super.blank(SemanticRoleKind.textField, semanticsObject) {
|
||||
: super.blank(EngineSemanticsRole.textField, semanticsObject) {
|
||||
_initializeEditableElement();
|
||||
}
|
||||
|
||||
|
@ -122,6 +122,9 @@ void runSemanticsTests() {
|
||||
group('link', () {
|
||||
_testLink();
|
||||
});
|
||||
group('tabs', () {
|
||||
_testTabs();
|
||||
});
|
||||
}
|
||||
|
||||
void _testSemanticRole() {
|
||||
@ -198,7 +201,7 @@ void _testRoleLifecycle() {
|
||||
tester.expectSemantics('<sem role="button"></sem>');
|
||||
|
||||
final SemanticsObject node = owner().debugSemanticsTree![0]!;
|
||||
expect(node.semanticRole?.kind, SemanticRoleKind.button);
|
||||
expect(node.semanticRole?.kind, EngineSemanticsRole.button);
|
||||
expect(
|
||||
node.semanticRole?.debugSemanticBehaviorTypes,
|
||||
containsAll(<Type>[Focusable, Tappable, LabelAndValue]),
|
||||
@ -221,7 +224,7 @@ void _testRoleLifecycle() {
|
||||
tester.expectSemantics('<sem role="button">a label</sem>');
|
||||
|
||||
final SemanticsObject node = owner().debugSemanticsTree![0]!;
|
||||
expect(node.semanticRole?.kind, SemanticRoleKind.button);
|
||||
expect(node.semanticRole?.kind, EngineSemanticsRole.button);
|
||||
expect(
|
||||
node.semanticRole?.debugSemanticBehaviorTypes,
|
||||
containsAll(<Type>[Focusable, Tappable, LabelAndValue]),
|
||||
@ -653,7 +656,7 @@ void _testEngineSemanticsOwner() {
|
||||
);
|
||||
|
||||
// Rudely replace the role with a mock, and trigger an update.
|
||||
final MockRole mockRole = MockRole(SemanticRoleKind.generic, semanticsObject);
|
||||
final MockRole mockRole = MockRole(EngineSemanticsRole.generic, semanticsObject);
|
||||
semanticsObject.semanticRole = mockRole;
|
||||
|
||||
pumpSemantics(label: 'World');
|
||||
@ -869,7 +872,7 @@ void _testText() {
|
||||
expectSemanticsTree(owner(), '''<sem><span>plain text</span></sem>''');
|
||||
|
||||
final SemanticsObject node = owner().debugSemanticsTree![0]!;
|
||||
expect(node.semanticRole?.kind, SemanticRoleKind.generic);
|
||||
expect(node.semanticRole?.kind, EngineSemanticsRole.generic);
|
||||
expect(node.semanticRole!.behaviors!.map((m) => m.runtimeType).toList(), <Type>[
|
||||
Focusable,
|
||||
LiveRegion,
|
||||
@ -896,7 +899,7 @@ void _testText() {
|
||||
expectSemanticsTree(owner(), '''<sem flt-tappable=""><span>tappable text</span></sem>''');
|
||||
|
||||
final SemanticsObject node = owner().debugSemanticsTree![0]!;
|
||||
expect(node.semanticRole?.kind, SemanticRoleKind.generic);
|
||||
expect(node.semanticRole?.kind, EngineSemanticsRole.generic);
|
||||
expect(node.semanticRole!.behaviors!.map((m) => m.runtimeType).toList(), <Type>[
|
||||
Focusable,
|
||||
LiveRegion,
|
||||
@ -1686,7 +1689,7 @@ void _testIncrementables() {
|
||||
</sem>''');
|
||||
|
||||
final SemanticsObject node = owner().debugSemanticsTree![0]!;
|
||||
expect(node.semanticRole?.kind, SemanticRoleKind.incrementable);
|
||||
expect(node.semanticRole?.kind, EngineSemanticsRole.incrementable);
|
||||
expect(
|
||||
reason: 'Incrementables use custom focus management',
|
||||
node.semanticRole!.debugSemanticBehaviorTypes,
|
||||
@ -1881,7 +1884,7 @@ void _testTextField() {
|
||||
// https://github.com/flutter/flutter/issues/147200
|
||||
expect(inputElement.value, '');
|
||||
|
||||
expect(node.semanticRole?.kind, SemanticRoleKind.textField);
|
||||
expect(node.semanticRole?.kind, EngineSemanticsRole.textField);
|
||||
expect(
|
||||
reason: 'Text fields use custom focus management',
|
||||
node.semanticRole!.debugSemanticBehaviorTypes,
|
||||
@ -1920,7 +1923,7 @@ void _testCheckables() {
|
||||
''');
|
||||
|
||||
final SemanticsObject node = owner().debugSemanticsTree![0]!;
|
||||
expect(node.semanticRole?.kind, SemanticRoleKind.checkable);
|
||||
expect(node.semanticRole?.kind, EngineSemanticsRole.checkable);
|
||||
expect(
|
||||
reason: 'Checkables use generic semantic behaviors',
|
||||
node.semanticRole!.debugSemanticBehaviorTypes,
|
||||
@ -2294,7 +2297,7 @@ void _testSelectables() {
|
||||
expectSemanticsTree(owner(), '<sem flt-tappable role="checkbox" aria-checked="true"></sem>');
|
||||
|
||||
final node = owner().debugSemanticsTree![0]!;
|
||||
expect(node.semanticRole!.kind, SemanticRoleKind.checkable);
|
||||
expect(node.semanticRole!.kind, EngineSemanticsRole.checkable);
|
||||
expect(node.semanticRole!.debugSemanticBehaviorTypes, isNot(contains(Selectable)));
|
||||
expect(node.element.getAttribute('aria-selected'), isNull);
|
||||
|
||||
@ -2325,7 +2328,7 @@ void _testTappable() {
|
||||
''');
|
||||
|
||||
final SemanticsObject node = owner().debugSemanticsTree![0]!;
|
||||
expect(node.semanticRole?.kind, SemanticRoleKind.button);
|
||||
expect(node.semanticRole?.kind, EngineSemanticsRole.button);
|
||||
expect(node.semanticRole?.debugSemanticBehaviorTypes, containsAll(<Type>[Focusable, Tappable]));
|
||||
expect(tester.getSemanticsObject(0).element.tabIndex, 0);
|
||||
|
||||
@ -3010,7 +3013,7 @@ void _testRoute() {
|
||||
<sem role="dialog" aria-label="this is a route label"><sem-c><sem></sem></sem-c></sem>
|
||||
''');
|
||||
|
||||
expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, SemanticRoleKind.route);
|
||||
expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, EngineSemanticsRole.route);
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
});
|
||||
@ -3049,7 +3052,7 @@ void _testRoute() {
|
||||
<sem role="dialog" aria-label=""><sem-c><sem></sem></sem-c></sem>
|
||||
''');
|
||||
|
||||
expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, SemanticRoleKind.route);
|
||||
expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, EngineSemanticsRole.route);
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
});
|
||||
@ -3091,8 +3094,8 @@ void _testRoute() {
|
||||
|
||||
pumpSemantics(label: 'Route label');
|
||||
|
||||
expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, SemanticRoleKind.route);
|
||||
expect(owner().debugSemanticsTree![2]!.semanticRole?.kind, SemanticRoleKind.generic);
|
||||
expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, EngineSemanticsRole.route);
|
||||
expect(owner().debugSemanticsTree![2]!.semanticRole?.kind, EngineSemanticsRole.generic);
|
||||
expect(
|
||||
owner().debugSemanticsTree![2]!.semanticRole?.debugSemanticBehaviorTypes,
|
||||
contains(RouteName),
|
||||
@ -3116,7 +3119,7 @@ void _testRoute() {
|
||||
<sem role="dialog"></sem>
|
||||
''');
|
||||
|
||||
expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, SemanticRoleKind.route);
|
||||
expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, EngineSemanticsRole.route);
|
||||
expect(owner().debugSemanticsTree![0]!.semanticRole?.behaviors, isNot(contains(RouteName)));
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
@ -3154,7 +3157,7 @@ void _testRoute() {
|
||||
</sem>
|
||||
''');
|
||||
|
||||
expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, SemanticRoleKind.generic);
|
||||
expect(owner().debugSemanticsTree![0]!.semanticRole?.kind, EngineSemanticsRole.generic);
|
||||
expect(
|
||||
owner().debugSemanticsTree![2]!.semanticRole?.debugSemanticBehaviorTypes,
|
||||
contains(RouteName),
|
||||
@ -3516,7 +3519,7 @@ void _testFocusable() {
|
||||
|
||||
final SemanticsObject node = owner().debugSemanticsTree![1]!;
|
||||
expect(node.isFocusable, isTrue);
|
||||
expect(node.semanticRole?.kind, SemanticRoleKind.generic);
|
||||
expect(node.semanticRole?.kind, EngineSemanticsRole.generic);
|
||||
expect(node.semanticRole?.debugSemanticBehaviorTypes, contains(Focusable));
|
||||
|
||||
final DomElement element = node.element;
|
||||
@ -3581,6 +3584,71 @@ void _testLink() {
|
||||
});
|
||||
}
|
||||
|
||||
void _testTabs() {
|
||||
test('nodes with tab role', () {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
SemanticsObject pumpSemantics() {
|
||||
final SemanticsTester tester = SemanticsTester(owner());
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
role: ui.SemanticsRole.tab,
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
return tester.getSemanticsObject(0);
|
||||
}
|
||||
|
||||
final SemanticsObject object = pumpSemantics();
|
||||
expect(object.semanticRole?.kind, EngineSemanticsRole.tab);
|
||||
expect(object.element.getAttribute('role'), 'tab');
|
||||
});
|
||||
|
||||
test('nodes with tab panel role', () {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
SemanticsObject pumpSemantics() {
|
||||
final SemanticsTester tester = SemanticsTester(owner());
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
role: ui.SemanticsRole.tabPanel,
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
return tester.getSemanticsObject(0);
|
||||
}
|
||||
|
||||
final SemanticsObject object = pumpSemantics();
|
||||
expect(object.semanticRole?.kind, EngineSemanticsRole.tabPanel);
|
||||
expect(object.element.getAttribute('role'), 'tabpanel');
|
||||
});
|
||||
|
||||
test('nodes with tab bar role', () {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
SemanticsObject pumpSemantics() {
|
||||
final SemanticsTester tester = SemanticsTester(owner());
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
role: ui.SemanticsRole.tabBar,
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
return tester.getSemanticsObject(0);
|
||||
}
|
||||
|
||||
final SemanticsObject object = pumpSemantics();
|
||||
expect(object.semanticRole?.kind, EngineSemanticsRole.tabList);
|
||||
expect(object.element.getAttribute('role'), 'tablist');
|
||||
});
|
||||
}
|
||||
|
||||
/// A facade in front of [ui.SemanticsUpdateBuilder.updateNode] that
|
||||
/// supplies default values for semantics attributes.
|
||||
void updateNode(
|
||||
|
@ -115,6 +115,7 @@ class SemanticsTester {
|
||||
List<SemanticsNodeUpdate>? children,
|
||||
int? headingLevel,
|
||||
String? linkUrl,
|
||||
ui.SemanticsRole? role,
|
||||
}) {
|
||||
// Flags
|
||||
if (hasCheckedState ?? false) {
|
||||
@ -323,6 +324,7 @@ class SemanticsTester {
|
||||
additionalActions: additionalActions ?? Int32List(0),
|
||||
headingLevel: headingLevel ?? 0,
|
||||
linkUrl: linkUrl,
|
||||
role: role ?? ui.SemanticsRole.none,
|
||||
);
|
||||
_nodeUpdates.add(update);
|
||||
return update;
|
||||
|
@ -46,7 +46,7 @@ Future<void> testMain() async {
|
||||
<sem><span>Hello</span></sem>''');
|
||||
|
||||
final SemanticsObject node = owner().debugSemanticsTree![0]!;
|
||||
expect(node.semanticRole?.kind, SemanticRoleKind.generic);
|
||||
expect(node.semanticRole?.kind, EngineSemanticsRole.generic);
|
||||
expect(
|
||||
reason: 'A node with a label should get a LabelAndValue role',
|
||||
node.semanticRole!.debugSemanticBehaviorTypes,
|
||||
|
@ -6,7 +6,7 @@
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' show lerpDouble;
|
||||
import 'dart:ui' show SemanticsRole, lerpDouble;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
@ -209,9 +209,12 @@ class Tab extends StatelessWidget implements PreferredSizeWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: height ?? calculatedHeight,
|
||||
child: Center(widthFactor: 1.0, child: label),
|
||||
return Semantics(
|
||||
role: SemanticsRole.tab,
|
||||
child: SizedBox(
|
||||
height: height ?? calculatedHeight,
|
||||
child: Center(widthFactor: 1.0, child: label),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1865,7 +1868,8 @@ class _TabBarState extends State<TabBar> {
|
||||
wrappedTabs[index],
|
||||
Semantics(
|
||||
selected: index == _currentIndex,
|
||||
label: localizations.tabLabel(tabIndex: index + 1, tabCount: tabCount),
|
||||
label:
|
||||
kIsWeb ? null : localizations.tabLabel(tabIndex: index + 1, tabCount: tabCount),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -1876,22 +1880,25 @@ class _TabBarState extends State<TabBar> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget tabBar = CustomPaint(
|
||||
painter: _indicatorPainter,
|
||||
child: _TabStyle(
|
||||
animation: kAlwaysDismissedAnimation,
|
||||
isSelected: false,
|
||||
isPrimary: widget._isPrimary,
|
||||
labelColor: widget.labelColor,
|
||||
unselectedLabelColor: widget.unselectedLabelColor,
|
||||
labelStyle: widget.labelStyle,
|
||||
unselectedLabelStyle: widget.unselectedLabelStyle,
|
||||
defaults: _defaults,
|
||||
child: _TabLabelBar(
|
||||
onPerformLayout: _saveTabOffsets,
|
||||
mainAxisSize:
|
||||
effectiveTabAlignment == TabAlignment.fill ? MainAxisSize.max : MainAxisSize.min,
|
||||
children: wrappedTabs,
|
||||
Widget tabBar = Semantics(
|
||||
role: SemanticsRole.tabBar,
|
||||
child: CustomPaint(
|
||||
painter: _indicatorPainter,
|
||||
child: _TabStyle(
|
||||
animation: kAlwaysDismissedAnimation,
|
||||
isSelected: false,
|
||||
isPrimary: widget._isPrimary,
|
||||
labelColor: widget.labelColor,
|
||||
unselectedLabelColor: widget.unselectedLabelColor,
|
||||
labelStyle: widget.labelStyle,
|
||||
unselectedLabelStyle: widget.unselectedLabelStyle,
|
||||
defaults: _defaults,
|
||||
child: _TabLabelBar(
|
||||
onPerformLayout: _saveTabOffsets,
|
||||
mainAxisSize:
|
||||
effectiveTabAlignment == TabAlignment.fill ? MainAxisSize.max : MainAxisSize.min,
|
||||
children: wrappedTabs,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -2131,7 +2138,11 @@ class _TabBarViewState extends State<TabBarView> {
|
||||
}
|
||||
|
||||
void _updateChildren() {
|
||||
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children);
|
||||
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(
|
||||
widget.children.map<Widget>((Widget child) {
|
||||
return Semantics(role: SemanticsRole.tabPanel, child: child);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleTabControllerAnimationTick() {
|
||||
|
@ -4430,6 +4430,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
|
||||
if (_properties.tagForChildren != null) {
|
||||
config.addTagForChildren(_properties.tagForChildren!);
|
||||
}
|
||||
if (properties.role != null) {
|
||||
config.role = _properties.role!;
|
||||
}
|
||||
// Registering _perform* as action handlers instead of the user provided
|
||||
// ones to ensure that changing a user provided handler from a non-null to
|
||||
// another non-null value doesn't require a semantics update.
|
||||
|
@ -16,6 +16,7 @@ import 'dart:ui'
|
||||
Rect,
|
||||
SemanticsAction,
|
||||
SemanticsFlag,
|
||||
SemanticsRole,
|
||||
SemanticsUpdate,
|
||||
SemanticsUpdateBuilder,
|
||||
StringAttribute,
|
||||
@ -481,6 +482,7 @@ class SemanticsData with Diagnosticable {
|
||||
required this.currentValueLength,
|
||||
required this.headingLevel,
|
||||
required this.linkUrl,
|
||||
required this.role,
|
||||
this.tags,
|
||||
this.transform,
|
||||
this.customSemanticsActionIds,
|
||||
@ -738,6 +740,9 @@ class SemanticsData with Diagnosticable {
|
||||
/// * [CustomSemanticsAction], for an explanation of custom actions.
|
||||
final List<int>? customSemanticsActionIds;
|
||||
|
||||
/// {@macro flutter.semantics.SemanticsNode.role}
|
||||
final SemanticsRole role;
|
||||
|
||||
/// Whether [flags] contains the given flag.
|
||||
bool hasFlag(SemanticsFlag flag) => (flags & flag.index) != 0;
|
||||
|
||||
@ -826,6 +831,7 @@ class SemanticsData with Diagnosticable {
|
||||
other.thickness == thickness &&
|
||||
other.headingLevel == headingLevel &&
|
||||
other.linkUrl == linkUrl &&
|
||||
other.role == role &&
|
||||
_sortedListsEqual(other.customSemanticsActionIds, customSemanticsActionIds);
|
||||
}
|
||||
|
||||
@ -859,6 +865,7 @@ class SemanticsData with Diagnosticable {
|
||||
headingLevel,
|
||||
linkUrl,
|
||||
customSemanticsActionIds == null ? null : Object.hashAll(customSemanticsActionIds!),
|
||||
role,
|
||||
),
|
||||
);
|
||||
|
||||
@ -1026,6 +1033,7 @@ class SemanticsProperties extends DiagnosticableTree {
|
||||
this.onFocus,
|
||||
this.onDismiss,
|
||||
this.customSemanticsActions,
|
||||
this.role,
|
||||
}) : assert(
|
||||
label == null || attributedLabel == null,
|
||||
'Only one of label or attributedLabel should be provided',
|
||||
@ -1799,6 +1807,19 @@ class SemanticsProperties extends DiagnosticableTree {
|
||||
/// * [CustomSemanticsAction], for an explanation of custom actions.
|
||||
final Map<CustomSemanticsAction, VoidCallback>? customSemanticsActions;
|
||||
|
||||
/// {@template flutter.semantics.SemanticsProperties.role}
|
||||
/// A enum to describe what role the subtree represents.
|
||||
///
|
||||
/// Setting the role for a widget subtree helps assistive technologies, such
|
||||
/// as screen readers, understand and interact with the UI correctly.
|
||||
///
|
||||
/// Defaults to [SemanticsRole.none] if not set, which means the subtree does
|
||||
/// not represent any complex ui or controls.
|
||||
///
|
||||
/// For a list of available roles, see [SemanticsRole].
|
||||
/// {@endtemplate}
|
||||
final SemanticsRole? role;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
@ -1835,6 +1856,7 @@ class SemanticsProperties extends DiagnosticableTree {
|
||||
properties.add(AttributedStringProperty('attributedHint', attributedHint, defaultValue: null));
|
||||
properties.add(StringProperty('tooltip', tooltip, defaultValue: null));
|
||||
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
|
||||
properties.add(EnumProperty<SemanticsRole>('role', role, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null));
|
||||
properties.add(
|
||||
DiagnosticsProperty<SemanticsHintOverrides>(
|
||||
@ -2392,6 +2414,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
_mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants ||
|
||||
_areUserActionsBlocked != config.isBlockingUserActions ||
|
||||
_headingLevel != config._headingLevel ||
|
||||
_linkUrl != config._linkUrl ||
|
||||
_linkUrl != config._linkUrl;
|
||||
}
|
||||
|
||||
@ -2708,6 +2731,17 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
Uri? get linkUrl => _linkUrl;
|
||||
Uri? _linkUrl = _kEmptyConfig._linkUrl;
|
||||
|
||||
/// {@template flutter.semantics.SemanticsNode.role}
|
||||
/// The role this node represents
|
||||
///
|
||||
/// A semantics node's role helps assistive technologies, such as screen
|
||||
/// readers, understand and interact with the UI correctly.
|
||||
///
|
||||
/// For a list of possible roles, see [SemanticsRole].
|
||||
/// {@endtemplate}
|
||||
SemanticsRole get role => _role;
|
||||
SemanticsRole _role = _kEmptyConfig.role;
|
||||
|
||||
bool _canPerformAction(SemanticsAction action) => _actions.containsKey(action);
|
||||
|
||||
static final SemanticsConfiguration _kEmptyConfig = SemanticsConfiguration();
|
||||
@ -2773,6 +2807,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
_areUserActionsBlocked = config.isBlockingUserActions;
|
||||
_headingLevel = config._headingLevel;
|
||||
_linkUrl = config._linkUrl;
|
||||
_role = config._role;
|
||||
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
|
||||
|
||||
if (mergeAllDescendantsIntoThisNodeValueChanged) {
|
||||
@ -2821,6 +2856,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
final double elevation = _elevation;
|
||||
double thickness = _thickness;
|
||||
Uri? linkUrl = _linkUrl;
|
||||
SemanticsRole role = _role;
|
||||
final Set<int> customSemanticsActionIds = <int>{};
|
||||
for (final CustomSemanticsAction action in _customSemanticsActions.keys) {
|
||||
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
|
||||
@ -2876,6 +2912,9 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
if (attributedDecreasedValue.string == '') {
|
||||
attributedDecreasedValue = node._attributedDecreasedValue;
|
||||
}
|
||||
if (role == SemanticsRole.none) {
|
||||
role = node._role;
|
||||
}
|
||||
if (tooltip == '') {
|
||||
tooltip = node._tooltip;
|
||||
}
|
||||
@ -2949,6 +2988,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
customSemanticsActionIds: customSemanticsActionIds.toList()..sort(),
|
||||
headingLevel: headingLevel,
|
||||
linkUrl: linkUrl,
|
||||
role: role,
|
||||
);
|
||||
}
|
||||
|
||||
@ -3026,6 +3066,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
additionalActions: customSemanticsActionIds ?? _kEmptyCustomSemanticsActionsList,
|
||||
headingLevel: data.headingLevel,
|
||||
linkUrl: data.linkUrl?.toString() ?? '',
|
||||
role: data.role,
|
||||
);
|
||||
_dirty = false;
|
||||
}
|
||||
@ -3202,6 +3243,9 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
properties.add(
|
||||
EnumProperty<TextDirection>('textDirection', _textDirection, defaultValue: null),
|
||||
);
|
||||
if (_role != SemanticsRole.none) {
|
||||
properties.add(EnumProperty<SemanticsRole>('role', _role));
|
||||
}
|
||||
properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null));
|
||||
if (_textSelection?.isValid ?? false) {
|
||||
properties.add(
|
||||
@ -4560,6 +4604,14 @@ class SemanticsConfiguration {
|
||||
_hasBeenAnnotated = true;
|
||||
}
|
||||
|
||||
/// {@macro flutter.semantics.SemanticsProperties.role}
|
||||
SemanticsRole get role => _role;
|
||||
SemanticsRole _role = SemanticsRole.none;
|
||||
set role(SemanticsRole value) {
|
||||
_role = value;
|
||||
_hasBeenAnnotated = true;
|
||||
}
|
||||
|
||||
/// A textual description of the owning [RenderObject].
|
||||
///
|
||||
/// Setting this attribute will override the [attributedLabel].
|
||||
@ -5313,6 +5365,9 @@ class SemanticsConfiguration {
|
||||
if (_attributedDecreasedValue.string == '') {
|
||||
_attributedDecreasedValue = child._attributedDecreasedValue;
|
||||
}
|
||||
if (_role == SemanticsRole.none) {
|
||||
_role = child._role;
|
||||
}
|
||||
_attributedHint = _concatAttributedString(
|
||||
thisAttributedString: _attributedHint,
|
||||
thisTextDirection: textDirection,
|
||||
@ -5365,7 +5420,8 @@ class SemanticsConfiguration {
|
||||
.._customSemanticsActions.addAll(_customSemanticsActions)
|
||||
..isBlockingUserActions = isBlockingUserActions
|
||||
.._headingLevel = _headingLevel
|
||||
.._linkUrl = _linkUrl;
|
||||
.._linkUrl = _linkUrl
|
||||
.._role = _role;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui show Image, ImageFilter, TextHeightBehavior;
|
||||
import 'dart:ui' as ui show Image, ImageFilter, SemanticsRole, TextHeightBehavior;
|
||||
|
||||
import 'package:flutter/animation.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -7288,6 +7288,7 @@ class Semantics extends SingleChildRenderObjectWidget {
|
||||
VoidCallback? onDidLoseAccessibilityFocus,
|
||||
VoidCallback? onFocus,
|
||||
Map<CustomSemanticsAction, VoidCallback>? customSemanticsActions,
|
||||
ui.SemanticsRole? role,
|
||||
}) : this.fromProperties(
|
||||
key: key,
|
||||
child: child,
|
||||
@ -7362,6 +7363,7 @@ class Semantics extends SingleChildRenderObjectWidget {
|
||||
onTapHint != null || onLongPressHint != null
|
||||
? SemanticsHintOverrides(onTapHint: onTapHint, onLongPressHint: onLongPressHint)
|
||||
: null,
|
||||
role: role,
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -3897,26 +3897,35 @@ void main() {
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
id: 4,
|
||||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasSelectedState,
|
||||
SemanticsFlag.isSelected,
|
||||
SemanticsFlag.isFocusable,
|
||||
rect: const Rect.fromLTRB(0.0, 0.0, 232.0, 600.0),
|
||||
role: SemanticsRole.tabBar,
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
id: 5,
|
||||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasSelectedState,
|
||||
SemanticsFlag.isSelected,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
label: 'TAB #0${kIsWeb ? '' : '\nTab 1 of 2'}',
|
||||
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
||||
role: SemanticsRole.tab,
|
||||
transform: Matrix4.translationValues(0.0, 276.0, 0.0),
|
||||
),
|
||||
TestSemantics(
|
||||
id: 6,
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasSelectedState,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||||
label: 'TAB #1${kIsWeb ? '' : '\nTab 2 of 2'}',
|
||||
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
||||
role: SemanticsRole.tab,
|
||||
transform: Matrix4.translationValues(116.0, 276.0, 0.0),
|
||||
),
|
||||
],
|
||||
label: 'TAB #0\nTab 1 of 2',
|
||||
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
||||
transform: Matrix4.translationValues(0.0, 276.0, 0.0),
|
||||
),
|
||||
TestSemantics(
|
||||
id: 5,
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasSelectedState,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||||
label: 'TAB #1\nTab 2 of 2',
|
||||
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
||||
transform: Matrix4.translationValues(116.0, 276.0, 0.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -3953,8 +3962,8 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
const String tab0title = 'This is a very wide tab #0\nTab 1 of 20';
|
||||
const String tab10title = 'This is a very wide tab #10\nTab 11 of 20';
|
||||
const String tab0title = 'This is a very wide tab #0${kIsWeb ? '' : '\nTab 1 of 20'}';
|
||||
const String tab10title = 'This is a very wide tab #10${kIsWeb ? '' : '\nTab 11 of 20'}';
|
||||
|
||||
const List<SemanticsFlag> hiddenFlags = <SemanticsFlag>[
|
||||
SemanticsFlag.isHidden,
|
||||
@ -4165,26 +4174,35 @@ void main() {
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
id: 4,
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasSelectedState,
|
||||
SemanticsFlag.isSelected,
|
||||
SemanticsFlag.isFocusable,
|
||||
rect: const Rect.fromLTRB(0.0, 0.0, 232.0, 600.0),
|
||||
role: SemanticsRole.tabBar,
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
id: 5,
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasSelectedState,
|
||||
SemanticsFlag.isSelected,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||||
label: 'Semantics override 0${kIsWeb ? '' : '\nTab 1 of 2'}',
|
||||
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
||||
role: SemanticsRole.tab,
|
||||
transform: Matrix4.translationValues(0.0, 276.0, 0.0),
|
||||
),
|
||||
TestSemantics(
|
||||
id: 6,
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasSelectedState,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||||
label: 'Semantics override 1${kIsWeb ? '' : '\nTab 2 of 2'}',
|
||||
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
||||
role: SemanticsRole.tab,
|
||||
transform: Matrix4.translationValues(116.0, 276.0, 0.0),
|
||||
),
|
||||
],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||||
label: 'Semantics override 0\nTab 1 of 2',
|
||||
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
||||
transform: Matrix4.translationValues(0.0, 276.0, 0.0),
|
||||
),
|
||||
TestSemantics(
|
||||
id: 5,
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasSelectedState,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||||
label: 'Semantics override 1\nTab 2 of 2',
|
||||
rect: const Rect.fromLTRB(0.0, 0.0, 116.0, kTextTabBarHeight),
|
||||
transform: Matrix4.translationValues(116.0, 276.0, 0.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -5982,9 +6000,10 @@ void main() {
|
||||
label: 'Tab 1 of 2',
|
||||
id: 1,
|
||||
rect: TestSemantics.fullScreen,
|
||||
role: SemanticsRole.tabBar,
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
label: 'TAB1\nTab 1 of 2',
|
||||
label: 'TAB1${kIsWeb ? '' : '\nTab 1 of 2'}',
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.isFocusable,
|
||||
SemanticsFlag.isSelected,
|
||||
@ -5993,13 +6012,15 @@ void main() {
|
||||
id: 2,
|
||||
rect: TestSemantics.fullScreen,
|
||||
actions: 1 | SemanticsAction.focus.index,
|
||||
role: SemanticsRole.tab,
|
||||
),
|
||||
TestSemantics(
|
||||
label: 'TAB2\nTab 2 of 2',
|
||||
label: 'TAB2${kIsWeb ? '' : '\nTab 2 of 2'}',
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isFocusable, SemanticsFlag.hasSelectedState],
|
||||
id: 3,
|
||||
rect: TestSemantics.fullScreen,
|
||||
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
||||
role: SemanticsRole.tab,
|
||||
),
|
||||
TestSemantics(
|
||||
id: 4,
|
||||
@ -6010,7 +6031,12 @@ void main() {
|
||||
rect: TestSemantics.fullScreen,
|
||||
actions: <SemanticsAction>[SemanticsAction.scrollLeft],
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(id: 5, rect: TestSemantics.fullScreen, label: 'PAGE1'),
|
||||
TestSemantics(
|
||||
id: 5,
|
||||
rect: TestSemantics.fullScreen,
|
||||
label: 'PAGE1',
|
||||
role: SemanticsRole.tabPanel,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
@ -228,6 +228,7 @@ class SemanticsUpdateBuilderSpy extends Fake implements ui.SemanticsUpdateBuilde
|
||||
required Int32List additionalActions,
|
||||
int headingLevel = 0,
|
||||
String? linkUrl,
|
||||
ui.SemanticsRole role = ui.SemanticsRole.none,
|
||||
}) {
|
||||
// Makes sure we don't send the same id twice.
|
||||
assert(!observations.containsKey(id));
|
||||
|
@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/physics.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
@ -53,6 +55,7 @@ class TestSemantics {
|
||||
this.scrollIndex,
|
||||
this.scrollChildren,
|
||||
Iterable<SemanticsTag>? tags,
|
||||
this.role = SemanticsRole.none,
|
||||
}) : assert(flags is int || flags is List<SemanticsFlag>),
|
||||
assert(actions is int || actions is List<SemanticsAction>),
|
||||
tags = tags?.toSet() ?? <SemanticsTag>{};
|
||||
@ -76,6 +79,7 @@ class TestSemantics {
|
||||
this.scrollIndex,
|
||||
this.scrollChildren,
|
||||
Iterable<SemanticsTag>? tags,
|
||||
this.role = SemanticsRole.none,
|
||||
}) : id = 0,
|
||||
assert(flags is int || flags is List<SemanticsFlag>),
|
||||
assert(actions is int || actions is List<SemanticsAction>),
|
||||
@ -115,6 +119,7 @@ class TestSemantics {
|
||||
this.scrollIndex,
|
||||
this.scrollChildren,
|
||||
Iterable<SemanticsTag>? tags,
|
||||
this.role = SemanticsRole.none,
|
||||
}) : assert(flags is int || flags is List<SemanticsFlag>),
|
||||
assert(actions is int || actions is List<SemanticsAction>),
|
||||
transform = _applyRootChildScale(transform),
|
||||
@ -243,6 +248,11 @@ class TestSemantics {
|
||||
|
||||
final int? headingLevel;
|
||||
|
||||
/// The expected role for the node.
|
||||
///
|
||||
/// Defaults to SemanticsRole.none if not set.
|
||||
final SemanticsRole role;
|
||||
|
||||
bool _matches(
|
||||
SemanticsNode? node,
|
||||
Map<dynamic, dynamic> matchState, {
|
||||
@ -378,6 +388,10 @@ class TestSemantics {
|
||||
);
|
||||
}
|
||||
|
||||
if (role != node.role) {
|
||||
return fail('expected node id $id to have role $role but found role ${node.role}');
|
||||
}
|
||||
|
||||
if (children.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
@ -781,6 +795,9 @@ class SemanticsTester {
|
||||
if (node.textDirection != null) {
|
||||
buf.writeln(' textDirection: ${node.textDirection},');
|
||||
}
|
||||
if (node.role != SemanticsRole.none) {
|
||||
buf.writeln(' role: ${node.role},');
|
||||
}
|
||||
if (node.hasChildren) {
|
||||
buf.writeln(' children: <TestSemantics>[');
|
||||
for (final SemanticsNode child in node.debugListChildrenInOrder(childOrder)) {
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
@ -717,6 +718,7 @@ void main() {
|
||||
maxValueLength: 15,
|
||||
headingLevel: 0,
|
||||
linkUrl: Uri(path: 'l'),
|
||||
role: ui.SemanticsRole.none,
|
||||
);
|
||||
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
|
||||
|
||||
@ -1015,6 +1017,7 @@ void main() {
|
||||
maxValueLength: 15,
|
||||
headingLevel: 0,
|
||||
linkUrl: Uri(path: 'l'),
|
||||
role: ui.SemanticsRole.none,
|
||||
);
|
||||
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
|
||||
|
||||
@ -1110,6 +1113,7 @@ void main() {
|
||||
maxValueLength: 15,
|
||||
headingLevel: 0,
|
||||
linkUrl: null,
|
||||
role: ui.SemanticsRole.none,
|
||||
);
|
||||
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
|
||||
|
||||
@ -1212,6 +1216,7 @@ void main() {
|
||||
maxValueLength: 15,
|
||||
headingLevel: 0,
|
||||
linkUrl: null,
|
||||
role: ui.SemanticsRole.none,
|
||||
);
|
||||
final _FakeSemanticsNode emptyNode = _FakeSemanticsNode(emptyData);
|
||||
|
||||
@ -1242,6 +1247,7 @@ void main() {
|
||||
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
|
||||
headingLevel: 0,
|
||||
linkUrl: Uri(path: 'l'),
|
||||
role: ui.SemanticsRole.none,
|
||||
);
|
||||
final _FakeSemanticsNode fullNode = _FakeSemanticsNode(fullData);
|
||||
|
||||
@ -1328,6 +1334,7 @@ void main() {
|
||||
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
|
||||
headingLevel: 0,
|
||||
linkUrl: null,
|
||||
role: ui.SemanticsRole.none,
|
||||
);
|
||||
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user