mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Makes Tappable semantics behavior adaptive (#182167)
<!-- 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/175119 mainly change how tappable and other classes work so that it can self shutoff when the semantics object change ## 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]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- 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
e1a890d577
commit
6ac6d3ec0f
@ -157,8 +157,11 @@ class Selectable extends SemanticBehavior {
|
||||
|
||||
@override
|
||||
void update() {
|
||||
// Do not use the [Selectable] behavior on checkables. Checkables use
|
||||
// special ARIA roles and `aria-checked`. Adding `aria-selected` in addition
|
||||
// to `aria-checked` would be confusing.
|
||||
if (semanticsObject.isFlagsDirty) {
|
||||
if (semanticsObject.isSelectable) {
|
||||
if (semanticsObject.isSelectable && !semanticsObject.isCheckable) {
|
||||
final ui.SemanticsRole currentRole = semanticsObject.role;
|
||||
final bool isSelected = semanticsObject.isSelected;
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ class SemanticScrollable extends SemanticRole {
|
||||
/// content under this scrollable area. This element is sized based on the
|
||||
/// total scroll extent calculated by scrollExtentMax - scrollExtentMin + rect.height
|
||||
/// of the [SemanticsObject] managed by this scrollable.
|
||||
final DomElement _scrollOverflowElement = createDomElement('flt-semantics-scroll-overflow');
|
||||
DomElement? _scrollOverflowElement;
|
||||
|
||||
/// Listens to HTML "scroll" gestures detected by the browser.
|
||||
///
|
||||
@ -109,18 +109,24 @@ class SemanticScrollable extends SemanticRole {
|
||||
// that may still experience overscroll issues when macOS inserts scrollbars
|
||||
// into the application.
|
||||
semanticsObject.element.style.scrollbarWidth = 'none';
|
||||
|
||||
_scrollOverflowElement.style
|
||||
..position = 'absolute'
|
||||
..transformOrigin = '0 0 0'
|
||||
// Ignore pointer events since this is a dummy element.
|
||||
..pointerEvents = 'none';
|
||||
append(_scrollOverflowElement);
|
||||
}
|
||||
|
||||
@override
|
||||
void update() {
|
||||
super.update();
|
||||
if (!_canScroll) {
|
||||
_cleanUp();
|
||||
return;
|
||||
}
|
||||
if (_scrollOverflowElement == null) {
|
||||
_scrollOverflowElement = createDomElement('flt-semantics-scroll-overflow');
|
||||
_scrollOverflowElement!.style
|
||||
..position = 'absolute'
|
||||
..transformOrigin = '0 0 0'
|
||||
// Ignore pointer events since this is a dummy element.
|
||||
..pointerEvents = 'none';
|
||||
append(_scrollOverflowElement!);
|
||||
}
|
||||
|
||||
semanticsObject.owner.addOneTimePostUpdateCallback(() {
|
||||
if (_canScroll) {
|
||||
@ -197,7 +203,7 @@ class SemanticScrollable extends SemanticRole {
|
||||
// and size it based on the total scroll extent so the browser
|
||||
// knows how much scrollable content there is.
|
||||
if (semanticsObject.isVerticalScrollContainer) {
|
||||
_scrollOverflowElement.style
|
||||
_scrollOverflowElement!.style
|
||||
// The cross axis size should be non-zero so it is taken into
|
||||
// account in the scrollable elements scrollHeight.
|
||||
..width = '1px'
|
||||
@ -206,7 +212,7 @@ class SemanticScrollable extends SemanticRole {
|
||||
..verticalScrollAdjustment = element.scrollTop
|
||||
..horizontalScrollAdjustment = 0.0;
|
||||
} else if (semanticsObject.isHorizontalScrollContainer) {
|
||||
_scrollOverflowElement.style
|
||||
_scrollOverflowElement!.style
|
||||
..width = '${scrollExtentTotal.toStringAsFixed(1)}px'
|
||||
// The cross axis size should be non-zero so it is taken into
|
||||
// account in the scrollable elements scrollHeight.
|
||||
@ -215,7 +221,7 @@ class SemanticScrollable extends SemanticRole {
|
||||
..verticalScrollAdjustment = 0.0
|
||||
..horizontalScrollAdjustment = element.scrollLeft;
|
||||
} else {
|
||||
_scrollOverflowElement.style
|
||||
_scrollOverflowElement!.style
|
||||
..transform = 'translate(0px,0px)'
|
||||
..width = '0px'
|
||||
..height = '0px';
|
||||
@ -257,8 +263,13 @@ class SemanticScrollable extends SemanticRole {
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_cleanUp();
|
||||
}
|
||||
|
||||
void _cleanUp() {
|
||||
_scrollOverflowElement?.remove();
|
||||
_scrollOverflowElement = null;
|
||||
final DomCSSStyleDeclaration style = element.style;
|
||||
assert(_gestureModeListener != null);
|
||||
style.removeProperty('overflowY');
|
||||
style.removeProperty('overflowX');
|
||||
style.removeProperty('touch-action');
|
||||
|
||||
@ -848,12 +848,7 @@ abstract class SemanticRole {
|
||||
|
||||
/// Adds the [Selectable] behavior, if the node is selectable but not checkable.
|
||||
void addSelectableBehavior() {
|
||||
// Do not use the [Selectable] behavior on checkables. Checkables use
|
||||
// special ARIA roles and `aria-checked`. Adding `aria-selected` in addition
|
||||
// to `aria-checked` would be confusing.
|
||||
if (semanticsObject.isSelectable && !semanticsObject.isCheckable) {
|
||||
addSemanticBehavior(Selectable(semanticsObject, this));
|
||||
}
|
||||
addSemanticBehavior(Selectable(semanticsObject, this));
|
||||
}
|
||||
|
||||
void addExpandableBehavior() {
|
||||
@ -1066,9 +1061,7 @@ final class GenericRole extends SemanticRole {
|
||||
// tappable. For example, the dismiss barrier of a pop-up menu is a tappable
|
||||
// ancestor of the menu itself, while the menu may contain tappable
|
||||
// children.
|
||||
if (semanticsObject.isTappable) {
|
||||
addTappable();
|
||||
}
|
||||
addTappable();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -43,45 +43,47 @@ class SemanticButton extends SemanticRole {
|
||||
///
|
||||
/// See also [ClickDebouncer].
|
||||
class Tappable extends SemanticBehavior {
|
||||
Tappable(super.semanticsObject, super.owner) {
|
||||
_clickListener = createDomEventListener((DomEvent click) {
|
||||
PointerBinding.clickDebouncer.onClick(click, viewId, semanticsObject.id, _isListening);
|
||||
});
|
||||
owner.element.addEventListener('click', _clickListener);
|
||||
}
|
||||
Tappable(super.semanticsObject, super.owner);
|
||||
|
||||
@override
|
||||
bool get shouldAcceptPointerEvents => true;
|
||||
bool get shouldAcceptPointerEvents => _isListening;
|
||||
|
||||
DomEventListener? _clickListener;
|
||||
bool _isListening = false;
|
||||
|
||||
@override
|
||||
void update() {
|
||||
final bool wasListening = _isListening;
|
||||
_isListening =
|
||||
final bool shouldListen =
|
||||
semanticsObject.enabledState() != EnabledState.disabled && semanticsObject.isTappable;
|
||||
if (wasListening != _isListening) {
|
||||
_updateAttribute();
|
||||
|
||||
if (_isListening == shouldListen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldListen) {
|
||||
_clickListener = createDomEventListener((DomEvent click) {
|
||||
PointerBinding.clickDebouncer.onClick(click, viewId, semanticsObject.id, _isListening);
|
||||
});
|
||||
owner.element.addEventListener('click', _clickListener);
|
||||
owner.element.setAttribute('flt-tappable', '');
|
||||
_isListening = true;
|
||||
} else {
|
||||
_cleanUp();
|
||||
}
|
||||
}
|
||||
|
||||
void _updateAttribute() {
|
||||
// The `flt-tappable` attribute marks the element for the ClickDebouncer to
|
||||
// to know that it should debounce click events on this element. The
|
||||
// contract is that the element that has this attribute is also the element
|
||||
// that receives pointer and "click" events.
|
||||
if (_isListening) {
|
||||
owner.element.setAttribute('flt-tappable', '');
|
||||
} else {
|
||||
owner.element.removeAttribute('flt-tappable');
|
||||
}
|
||||
void _cleanUp() {
|
||||
owner.element.removeEventListener('click', _clickListener);
|
||||
owner.element.removeAttribute('flt-tappable');
|
||||
_clickListener = null;
|
||||
_isListening = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
owner.removeEventListener('click', _clickListener);
|
||||
_clickListener = null;
|
||||
if (_isListening) {
|
||||
_cleanUp();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
// 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 'dart:async';
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../../common/test_initialization.dart';
|
||||
import 'semantics_tester.dart';
|
||||
|
||||
DateTime _testTime = DateTime(2018, 12, 17);
|
||||
|
||||
EngineSemantics semantics() => EngineSemantics.instance;
|
||||
EngineSemanticsOwner owner() => EnginePlatformDispatcher.instance.implicitView!.semantics;
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() {
|
||||
return testMain;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> testMain() async {
|
||||
await bootstrapAndRunApp(withImplicitView: true);
|
||||
setUp(() {
|
||||
EngineSemantics.debugResetSemantics();
|
||||
});
|
||||
|
||||
test('SemanticScrollable transitions from non-scrollable to scrollable', () async {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
final tester = SemanticsTester(owner());
|
||||
|
||||
// Initial state: Generic node, NOT scrollable
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
flags: const ui.SemanticsFlags(
|
||||
isEnabled: ui.Tristate.isTrue,
|
||||
// No scroll container flags
|
||||
),
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 100),
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
// Should NOT have flt-semantics-scroll-overflow
|
||||
expectSemanticsTree(owner(), '<sem></sem>');
|
||||
expect(owner().semanticsHost.querySelector('flt-semantics-scroll-overflow'), isNull);
|
||||
|
||||
// Make it Vertical Scrollable
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
flags: const ui.SemanticsFlags(isEnabled: ui.Tristate.isTrue, hasImplicitScrolling: true),
|
||||
scrollChildren: 10,
|
||||
scrollIndex: 0,
|
||||
scrollExtentMin: 0.0,
|
||||
scrollExtentMax: 500.0,
|
||||
scrollPosition: 0.0,
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 100),
|
||||
hasScrollUp: true,
|
||||
hasScrollDown: true,
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
// Now it should be a scrollable and have the overflow element.
|
||||
expectSemanticsTree(
|
||||
owner(),
|
||||
'<sem role="group"><flt-semantics-scroll-overflow></flt-semantics-scroll-overflow></sem>',
|
||||
);
|
||||
expect(owner().semanticsHost.querySelector('flt-semantics-scroll-overflow'), isNotNull);
|
||||
|
||||
// Transition back to NON-scrollable provided (remove actions)
|
||||
// BUT keep hasImplicitScrolling: true so it stays as SemanticScrollable role
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
flags: const ui.SemanticsFlags(isEnabled: ui.Tristate.isTrue, hasImplicitScrolling: true),
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 100),
|
||||
// Implicitly actions = 0 (no scroll flags set)
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
// Should still have role="group" (from SemanticScrollable)
|
||||
expectSemanticsTree(owner(), '<sem role="group"></sem>');
|
||||
// Should NOT have overflow element because it cannot scroll
|
||||
expect(owner().semanticsHost.querySelector('flt-semantics-scroll-overflow'), isNull);
|
||||
|
||||
// Verify it is indeed SemanticScrollable
|
||||
expect(tester.getSemanticsObject(0).semanticRole, isA<SemanticScrollable>());
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,169 @@
|
||||
// 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 'dart:async';
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../../common/test_initialization.dart';
|
||||
import 'semantics_tester.dart';
|
||||
|
||||
DateTime _testTime = DateTime(2018, 12, 17);
|
||||
|
||||
EngineSemantics semantics() => EngineSemantics.instance;
|
||||
EngineSemanticsOwner owner() => EnginePlatformDispatcher.instance.implicitView!.semantics;
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() {
|
||||
return testMain;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> testMain() async {
|
||||
await bootstrapAndRunApp(withImplicitView: true);
|
||||
setUp(() {
|
||||
EngineSemantics.debugResetSemantics();
|
||||
});
|
||||
|
||||
test(
|
||||
'Selectable behavior adds/removes aria-current based on isSelectable flag for img role',
|
||||
() async {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
final tester = SemanticsTester(owner());
|
||||
|
||||
// Initial state: Image, NOT selectable
|
||||
// SemanticImage adds Selectable behavior in constructor.
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
flags: const ui.SemanticsFlags(isImage: true, isEnabled: ui.Tristate.isTrue),
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
// Should be an img
|
||||
expectSemanticsTree(owner(), '<sem role="img"></sem>');
|
||||
expect(owner().semanticsHost.querySelector('[aria-current]'), isNull);
|
||||
|
||||
// Make Selectable (hasSelectedState = true)
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
flags: const ui.SemanticsFlags(
|
||||
isImage: true,
|
||||
isEnabled: ui.Tristate.isTrue,
|
||||
isSelected: ui.Tristate.isFalse, // implies hasSelectedState
|
||||
),
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
// Should now have aria-current="false"
|
||||
expectSemanticsTree(owner(), '<sem role="img" aria-current="false"></sem>');
|
||||
expect(owner().semanticsHost.querySelector('[aria-current]'), isNotNull);
|
||||
|
||||
// Select it
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
flags: const ui.SemanticsFlags(
|
||||
isImage: true,
|
||||
isEnabled: ui.Tristate.isTrue,
|
||||
isSelected: ui.Tristate.isTrue, // implies hasSelectedState
|
||||
),
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
expectSemanticsTree(owner(), '<sem role="img" aria-current="true"></sem>');
|
||||
|
||||
// Make NOT Selectable again hiding "hasSelectedState" by setting isSelected to none
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
flags: const ui.SemanticsFlags(
|
||||
isImage: true,
|
||||
isEnabled: ui.Tristate.isTrue,
|
||||
// hasSelectedState removed (default is Tristate.none)
|
||||
),
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
// Should remove aria-current
|
||||
expectSemanticsTree(owner(), '<sem role="img"></sem>');
|
||||
expect(owner().semanticsHost.querySelector('[aria-current]'), isNull);
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'Selectable behavior adds/removes aria-selected based on isSelectable flag for tab role',
|
||||
() async {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
final tester = SemanticsTester(owner());
|
||||
|
||||
// Initial state: Tab, Selectable but NOT selected
|
||||
// SemanticTab adds Selectable behavior in constructor?
|
||||
// Let's assume we can just use GenericRole or manually constructed node with role tab?
|
||||
// Actually SemanticRole.tab exists?
|
||||
// Let's check constructor or how to spawn it.
|
||||
// UpdateNode with role: ui.SemanticsRole.tab should do it if mapping exists.
|
||||
// The test helper `updateNode` takes `role`.
|
||||
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
role: ui.SemanticsRole.tab,
|
||||
flags: const ui.SemanticsFlags(
|
||||
isEnabled: ui.Tristate.isTrue,
|
||||
isSelected: ui.Tristate.isFalse, // implies hasSelectedState (selectable)
|
||||
),
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
// Should be a tab
|
||||
expectSemanticsTree(owner(), '<sem role="tab" aria-selected="false"></sem>');
|
||||
expect(owner().semanticsHost.querySelector('[aria-selected="false"]'), isNotNull);
|
||||
|
||||
// Select it
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
role: ui.SemanticsRole.tab,
|
||||
flags: const ui.SemanticsFlags(
|
||||
isEnabled: ui.Tristate.isTrue,
|
||||
isSelected: ui.Tristate.isTrue,
|
||||
),
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
// Should be selected
|
||||
expectSemanticsTree(owner(), '<sem role="tab" aria-selected="true"></sem>');
|
||||
expect(owner().semanticsHost.querySelector('[aria-selected="true"]'), isNotNull);
|
||||
|
||||
// Make NOT Selectable
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
role: ui.SemanticsRole.tab,
|
||||
flags: const ui.SemanticsFlags(isEnabled: ui.Tristate.isTrue),
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
// Should remove aria-selected
|
||||
expectSemanticsTree(owner(), '<sem role="tab"></sem>');
|
||||
expect(owner().semanticsHost.querySelector('[aria-current]'), isNull);
|
||||
expect(owner().semanticsHost.querySelector('[aria-selected]'), isNull);
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -1669,7 +1669,11 @@ void _testContainer() {
|
||||
|
||||
test('checkable leaf nodes accept pointer events', () async {
|
||||
final builder = ui.SemanticsUpdateBuilder();
|
||||
updateNode(builder, flags: const ui.SemanticsFlags(isChecked: ui.CheckedState.isFalse));
|
||||
updateNode(
|
||||
builder,
|
||||
flags: const ui.SemanticsFlags(isChecked: ui.CheckedState.isFalse),
|
||||
actions: ui.SemanticsAction.tap.index,
|
||||
);
|
||||
|
||||
owner().updateSemantics(builder.build());
|
||||
|
||||
@ -1701,7 +1705,11 @@ void _testContainer() {
|
||||
|
||||
test('link leaf nodes accept pointer events', () async {
|
||||
final builder = ui.SemanticsUpdateBuilder();
|
||||
updateNode(builder, flags: const ui.SemanticsFlags(isLink: true));
|
||||
updateNode(
|
||||
builder,
|
||||
flags: const ui.SemanticsFlags(isLink: true),
|
||||
actions: ui.SemanticsAction.tap.index,
|
||||
);
|
||||
|
||||
owner().updateSemantics(builder.build());
|
||||
|
||||
@ -2163,6 +2171,7 @@ void _testVerticalScrolling() {
|
||||
updateNode(
|
||||
builder,
|
||||
flags: const ui.SemanticsFlags(hasImplicitScrolling: true),
|
||||
actions: ui.SemanticsAction.scrollUp.index,
|
||||
transform: Matrix4.identity().toFloat64(),
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 50, 100),
|
||||
);
|
||||
@ -2187,6 +2196,7 @@ void _testVerticalScrolling() {
|
||||
updateNode(
|
||||
builder,
|
||||
flags: const ui.SemanticsFlags(hasImplicitScrolling: true),
|
||||
actions: ui.SemanticsAction.scrollLeft.index, // Only have a horizontal scroll action.
|
||||
transform: Matrix4.identity().toFloat64(),
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 50, 100),
|
||||
);
|
||||
@ -3279,7 +3289,6 @@ void _testSelectables() {
|
||||
|
||||
final SemanticsObject node = owner().debugSemanticsTree![0]!;
|
||||
expect(node.semanticRole!.kind, EngineSemanticsRole.checkable);
|
||||
expect(node.semanticRole!.debugSemanticBehaviorTypes, isNot(contains(Selectable)));
|
||||
expect(node.element.getAttribute('aria-selected'), isNull);
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
|
||||
@ -0,0 +1,154 @@
|
||||
// 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 'dart:async';
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../../common/test_initialization.dart';
|
||||
import 'semantics_tester.dart';
|
||||
|
||||
DateTime _testTime = DateTime(2018, 12, 17);
|
||||
|
||||
EngineSemantics semantics() => EngineSemantics.instance;
|
||||
EngineSemanticsOwner owner() => EnginePlatformDispatcher.instance.implicitView!.semantics;
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() {
|
||||
return testMain;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> testMain() async {
|
||||
await bootstrapAndRunApp(withImplicitView: true);
|
||||
setUp(() {
|
||||
EngineSemantics.debugResetSemantics();
|
||||
});
|
||||
|
||||
test('Tappable adds and removes listener on update', () async {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
final tester = SemanticsTester(owner());
|
||||
|
||||
// Initial state: Tappable and Enabled
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
flags: const ui.SemanticsFlags(isButton: true, isEnabled: ui.Tristate.isTrue),
|
||||
hasTap: true,
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
expectSemanticsTree(owner(), '<sem role="button" flt-tappable></sem>');
|
||||
|
||||
// Scan for flt-tappable attribute in DOM
|
||||
final DomElement? element0 = owner().semanticsHost.querySelector('[flt-tappable]');
|
||||
expect(element0, isNotNull);
|
||||
|
||||
// DISABLED -> Should remove flt-tappable
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
flags: const ui.SemanticsFlags(isButton: true, isEnabled: ui.Tristate.isFalse),
|
||||
hasTap: true,
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
// When disabled, Tappable behavior removes flt-tappable
|
||||
expectSemanticsTree(owner(), '<sem role="button" aria-disabled="true"></sem>');
|
||||
expect(owner().semanticsHost.querySelector('[flt-tappable]'), isNull);
|
||||
|
||||
// ENABLED again -> Should add flt-tappable
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
flags: const ui.SemanticsFlags(isButton: true, isEnabled: ui.Tristate.isTrue),
|
||||
hasTap: true,
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
expectSemanticsTree(owner(), '<sem role="button" flt-tappable></sem>');
|
||||
expect(owner().semanticsHost.querySelector('[flt-tappable]'), isNotNull);
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
});
|
||||
|
||||
test('Tappable behavior stops listening when not tappable even if enabled', () async {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
final tester = SemanticsTester(owner());
|
||||
|
||||
// Initial state: Tappable and Enabled
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
flags: const ui.SemanticsFlags(isButton: true, isEnabled: ui.Tristate.isTrue),
|
||||
hasTap: true,
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
expectSemanticsTree(owner(), '<sem role="button" flt-tappable></sem>');
|
||||
|
||||
// Make NOT Tappable (remove hasTap, but keep isButton? isButton usually implies tappable but technically hasTap is the action)
|
||||
// Actually SemanticButton adds Tappable unconditionally now, but Tappable checks semanticsObject.isTappable.
|
||||
// semanticsObject.isTappable returns true if it has any tap-related actions or flags.
|
||||
|
||||
// Let's remove hasTap action.
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
flags: const ui.SemanticsFlags(isButton: true, isEnabled: ui.Tristate.isTrue),
|
||||
hasTap: false, // No tap action
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
// Should NOT have flt-tappable because isTappable should be false (assuming no other tappable flags/actions)
|
||||
// Note: isButton might imply some accessibility role but Tappable behavior checks semanticsObject.isTappable.
|
||||
expectSemanticsTree(owner(), '<sem role="button"></sem>');
|
||||
expect(owner().semanticsHost.querySelector('[flt-tappable]'), isNull);
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
});
|
||||
|
||||
test('GenericRole starts untappable and becomes tappable', () async {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
final tester = SemanticsTester(owner());
|
||||
|
||||
// Initial state: GenericRole (no specific role), Untappable
|
||||
// Use ID 1 to avoid conflict with previous test if state leaks (though setUp resets semantics)
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
flags: const ui.SemanticsFlags(isEnabled: ui.Tristate.isTrue),
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
// Should NOT have flt-tappable
|
||||
expectSemanticsTree(owner(), '<sem></sem>');
|
||||
expect(owner().semanticsHost.querySelector('[flt-tappable]'), isNull);
|
||||
|
||||
// Update to be Tappable
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
flags: const ui.SemanticsFlags(isEnabled: ui.Tristate.isTrue),
|
||||
hasTap: true,
|
||||
rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
// Should NOW have flt-tappable
|
||||
expectSemanticsTree(owner(), '<sem flt-tappable></sem>');
|
||||
expect(owner().semanticsHost.querySelector('[flt-tappable]'), isNotNull);
|
||||
|
||||
semantics().semanticsEnabled = false;
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user