[web] Send the correct view ID with semantics actions (flutter/engine#56595)
Currently, all semantics actions that we send to the framework contain `viewId: 0`. This leads to issues such as https://github.com/flutter/flutter/issues/158530 because the framework routes the pointer event to the wrong view. Let's send the correct view ID with semantics actions. Fixes https://github.com/flutter/flutter/issues/158530
This commit is contained in:
parent
453afd650f
commit
d27125f326
@ -1298,14 +1298,18 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
|
||||
/// Engine code should use this method instead of the callback directly.
|
||||
/// Otherwise zones won't work properly.
|
||||
void invokeOnSemanticsAction(
|
||||
int nodeId, ui.SemanticsAction action, ByteData? args) {
|
||||
int viewId,
|
||||
int nodeId,
|
||||
ui.SemanticsAction action,
|
||||
ByteData? args,
|
||||
) {
|
||||
invoke1<ui.SemanticsActionEvent>(
|
||||
_onSemanticsActionEvent,
|
||||
_onSemanticsActionEventZone,
|
||||
ui.SemanticsActionEvent(
|
||||
type: action,
|
||||
nodeId: nodeId,
|
||||
viewId: 0, // TODO(goderbauer): Wire up the real view ID.
|
||||
viewId: viewId,
|
||||
arguments: args,
|
||||
),
|
||||
);
|
||||
|
@ -271,7 +271,7 @@ class ClickDebouncer {
|
||||
/// Forwards the event to the framework, unless it is deduplicated because
|
||||
/// the corresponding pointer down/up events were recently flushed to the
|
||||
/// framework already.
|
||||
void onClick(DomEvent click, int semanticsNodeId, bool isListening) {
|
||||
void onClick(DomEvent click, int viewId, int semanticsNodeId, bool isListening) {
|
||||
assert(click.type == 'click');
|
||||
|
||||
if (!isDebouncing) {
|
||||
@ -280,7 +280,7 @@ class ClickDebouncer {
|
||||
// recently and if the node is currently listening to event, forward to
|
||||
// the framework.
|
||||
if (isListening && _shouldSendClickEventToFramework(click)) {
|
||||
_sendSemanticsTapToFramework(click, semanticsNodeId);
|
||||
_sendSemanticsTapToFramework(click, viewId, semanticsNodeId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -292,7 +292,7 @@ class ClickDebouncer {
|
||||
final DebounceState state = _state!;
|
||||
_state = null;
|
||||
state.timer.cancel();
|
||||
_sendSemanticsTapToFramework(click, semanticsNodeId);
|
||||
_sendSemanticsTapToFramework(click, viewId, semanticsNodeId);
|
||||
} else {
|
||||
// The semantic node is not listening to taps. Flush the pointer events
|
||||
// for the framework to figure out what to do with them. It's possible
|
||||
@ -301,7 +301,11 @@ class ClickDebouncer {
|
||||
}
|
||||
}
|
||||
|
||||
void _sendSemanticsTapToFramework(DomEvent click, int semanticsNodeId) {
|
||||
void _sendSemanticsTapToFramework(
|
||||
DomEvent click,
|
||||
int viewId,
|
||||
int semanticsNodeId,
|
||||
) {
|
||||
// Tappable nodes can be nested inside other tappable nodes. If a click
|
||||
// lands on an inner element and is allowed to propagate, it will also
|
||||
// land on the ancestor tappable, leading to both the descendant and the
|
||||
@ -312,7 +316,11 @@ class ClickDebouncer {
|
||||
click.stopPropagation();
|
||||
|
||||
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
|
||||
semanticsNodeId, ui.SemanticsAction.tap, null);
|
||||
viewId,
|
||||
semanticsNodeId,
|
||||
ui.SemanticsAction.tap,
|
||||
null,
|
||||
);
|
||||
reset();
|
||||
}
|
||||
|
||||
|
@ -206,6 +206,7 @@ class AccessibilityFocusManager {
|
||||
// shifting focus.
|
||||
if (_lastEvent != AccessibilityFocusManagerEvent.requestedFocus) {
|
||||
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
|
||||
_owner.viewId,
|
||||
target.semanticsNodeId,
|
||||
ui.SemanticsAction.focus,
|
||||
null,
|
||||
|
@ -43,11 +43,11 @@ class SemanticIncrementable extends SemanticRole {
|
||||
if (newInputValue > _currentSurrogateValue) {
|
||||
_currentSurrogateValue += 1;
|
||||
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
|
||||
semanticsObject.id, ui.SemanticsAction.increase, null);
|
||||
viewId, semanticsObject.id, ui.SemanticsAction.increase, null);
|
||||
} else if (newInputValue < _currentSurrogateValue) {
|
||||
_currentSurrogateValue -= 1;
|
||||
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
|
||||
semanticsObject.id, ui.SemanticsAction.decrease, null);
|
||||
viewId, semanticsObject.id, ui.SemanticsAction.decrease, null);
|
||||
}
|
||||
}));
|
||||
|
||||
|
@ -76,20 +76,20 @@ class SemanticScrollable extends SemanticRole {
|
||||
if (doScrollForward) {
|
||||
if (semanticsObject.isVerticalScrollContainer) {
|
||||
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
|
||||
semanticsId, ui.SemanticsAction.scrollUp, null);
|
||||
viewId, semanticsId, ui.SemanticsAction.scrollUp, null);
|
||||
} else {
|
||||
assert(semanticsObject.isHorizontalScrollContainer);
|
||||
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
|
||||
semanticsId, ui.SemanticsAction.scrollLeft, null);
|
||||
viewId, semanticsId, ui.SemanticsAction.scrollLeft, null);
|
||||
}
|
||||
} else {
|
||||
if (semanticsObject.isVerticalScrollContainer) {
|
||||
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
|
||||
semanticsId, ui.SemanticsAction.scrollDown, null);
|
||||
viewId, semanticsId, ui.SemanticsAction.scrollDown, null);
|
||||
} else {
|
||||
assert(semanticsObject.isHorizontalScrollContainer);
|
||||
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
|
||||
semanticsId, ui.SemanticsAction.scrollRight, null);
|
||||
viewId, semanticsId, ui.SemanticsAction.scrollRight, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -444,6 +444,9 @@ abstract class SemanticRole {
|
||||
/// The semantics object managed by this role.
|
||||
final SemanticsObject semanticsObject;
|
||||
|
||||
/// The ID of the Flutter View that this [SemanticRole] belongs to.
|
||||
int get viewId => semanticsObject.owner.viewId;
|
||||
|
||||
/// Whether this role accepts pointer events.
|
||||
///
|
||||
/// This boolean decides whether to set the `pointer-events` CSS property to
|
||||
@ -764,6 +767,9 @@ abstract class SemanticBehavior {
|
||||
|
||||
final SemanticRole owner;
|
||||
|
||||
/// The ID of the Flutter View that this [SemanticBehavior] belongs to.
|
||||
int get viewId => semanticsObject.owner.viewId;
|
||||
|
||||
/// Whether this role accepts pointer events.
|
||||
///
|
||||
/// This boolean decides whether to set the `pointer-events` CSS property to
|
||||
@ -2381,12 +2387,15 @@ class EngineSemantics {
|
||||
|
||||
/// The top-level service that manages everything semantics-related.
|
||||
class EngineSemanticsOwner {
|
||||
EngineSemanticsOwner(this.semanticsHost) {
|
||||
EngineSemanticsOwner(this.viewId, this.semanticsHost) {
|
||||
registerHotRestartListener(() {
|
||||
_rootSemanticsElement?.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/// The ID of the Flutter View that this semantics owner belongs to.
|
||||
final int viewId;
|
||||
|
||||
/// The permanent element in the view's DOM structure that hosts the semantics
|
||||
/// tree.
|
||||
///
|
||||
|
@ -46,6 +46,7 @@ class Tappable extends SemanticBehavior {
|
||||
_clickListener = createDomEventListener((DomEvent click) {
|
||||
PointerBinding.clickDebouncer.onClick(
|
||||
click,
|
||||
viewId,
|
||||
semanticsObject.id,
|
||||
_isListening,
|
||||
);
|
||||
|
@ -278,7 +278,7 @@ class SemanticTextField extends SemanticRole {
|
||||
// IMPORTANT: because this event listener can be triggered by either or
|
||||
// both a "focus" and a "click" DOM events, this code must be idempotent.
|
||||
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
|
||||
semanticsObject.id, ui.SemanticsAction.focus, null);
|
||||
viewId, semanticsObject.id, ui.SemanticsAction.focus, null);
|
||||
}));
|
||||
editableElement.addEventListener('click', createDomEventListener((DomEvent event) {
|
||||
editableElement.focusWithoutScroll();
|
||||
|
@ -157,7 +157,7 @@ class EngineFlutterView implements ui.FlutterView {
|
||||
|
||||
final JsViewConstraints? _jsViewConstraints;
|
||||
|
||||
late final EngineSemanticsOwner semantics = EngineSemanticsOwner(dom.semanticsHost);
|
||||
late final EngineSemanticsOwner semantics = EngineSemanticsOwner(viewId, dom.semanticsHost);
|
||||
|
||||
@override
|
||||
ui.Size get physicalSize {
|
||||
|
@ -3021,7 +3021,7 @@ void _testClickDebouncer({required PointerBinding Function() getBinding}) {
|
||||
}
|
||||
);
|
||||
|
||||
PointerBinding.clickDebouncer.onClick(click, 42, true);
|
||||
PointerBinding.clickDebouncer.onClick(click, view.viewId, 42, true);
|
||||
expect(PointerBinding.clickDebouncer.isDebouncing, false);
|
||||
expect(pointerPackets, isEmpty);
|
||||
expect(semanticsActions, <CapturedSemanticsEvent>[
|
||||
@ -3046,7 +3046,7 @@ void _testClickDebouncer({required PointerBinding Function() getBinding}) {
|
||||
}
|
||||
);
|
||||
|
||||
PointerBinding.clickDebouncer.onClick(click, 42, true);
|
||||
PointerBinding.clickDebouncer.onClick(click, view.viewId, 42, true);
|
||||
expect(pointerPackets, isEmpty);
|
||||
expect(semanticsActions, <CapturedSemanticsEvent>[
|
||||
(type: ui.SemanticsAction.tap, nodeId: 42)
|
||||
@ -3070,7 +3070,7 @@ void _testClickDebouncer({required PointerBinding Function() getBinding}) {
|
||||
}
|
||||
);
|
||||
|
||||
PointerBinding.clickDebouncer.onClick(click, 42, false);
|
||||
PointerBinding.clickDebouncer.onClick(click, view.viewId, 42, false);
|
||||
expect(
|
||||
reason: 'When tappable declares that it is not listening to click events '
|
||||
'the debouncer flushes the pointer events to the framework and '
|
||||
@ -3129,7 +3129,7 @@ void _testClickDebouncer({required PointerBinding Function() getBinding}) {
|
||||
'clientY': testElement.getBoundingClientRect().y,
|
||||
}
|
||||
);
|
||||
PointerBinding.clickDebouncer.onClick(click, 42, true);
|
||||
PointerBinding.clickDebouncer.onClick(click, view.viewId, 42, true);
|
||||
|
||||
expect(
|
||||
reason: 'Because the DOM click event was deduped.',
|
||||
@ -3190,7 +3190,7 @@ void _testClickDebouncer({required PointerBinding Function() getBinding}) {
|
||||
'clientY': testElement.getBoundingClientRect().y,
|
||||
}
|
||||
);
|
||||
PointerBinding.clickDebouncer.onClick(click, 42, true);
|
||||
PointerBinding.clickDebouncer.onClick(click, view.viewId, 42, true);
|
||||
|
||||
expect(
|
||||
reason: 'Because the DOM click event was deduped.',
|
||||
@ -3245,7 +3245,7 @@ void _testClickDebouncer({required PointerBinding Function() getBinding}) {
|
||||
'clientY': testElement.getBoundingClientRect().y,
|
||||
}
|
||||
);
|
||||
PointerBinding.clickDebouncer.onClick(click, 42, true);
|
||||
PointerBinding.clickDebouncer.onClick(click, view.viewId, 42, true);
|
||||
|
||||
expect(
|
||||
reason: 'The DOM click should still be sent to the framework because it '
|
||||
|
@ -239,7 +239,8 @@ Future<void> testMain() async {
|
||||
expect(ui.PlatformDispatcher.instance.onSemanticsActionEvent, same(callback));
|
||||
});
|
||||
|
||||
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(0, ui.SemanticsAction.tap, null);
|
||||
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
|
||||
myWindow.viewId, 0, ui.SemanticsAction.tap, null);
|
||||
});
|
||||
|
||||
test('onAccessibilityFeaturesChanged preserves the zone', () {
|
||||
|
Loading…
x
Reference in New Issue
Block a user