[web:a11y] wheel events switch to pointer mode (#163582)

Fixes https://github.com/flutter/flutter/issues/159358
This commit is contained in:
Yegor 2025-02-19 13:33:04 -08:00 committed by GitHub
parent dddb232e70
commit 7e297c29f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 111 additions and 0 deletions

View File

@ -723,6 +723,13 @@ mixin _WheelEventListenerMixin on _BaseAdapter {
}
void _handleWheelEvent(DomEvent event) {
// Wheel events should switch semantics to pointer event mode, because wheel
// events should always be handled by the framework.
// See: https://github.com/flutter/flutter/issues/159358
if (!EngineSemantics.instance.receiveGlobalEvent(event)) {
return;
}
assert(domInstanceOfString(event, 'WheelEvent'));
if (_debugLogPointerEvents) {
print(event.type);

View File

@ -2327,6 +2327,15 @@ class EngineSemantics {
GestureMode get gestureMode => _gestureMode;
GestureMode _gestureMode = GestureMode.browserGestures;
/// Resets [gestureMode] back to its original value [GestureMode.browserGestures].
///
/// This is intended to be used in tests only.
@visibleForTesting
void debugResetGestureMode() {
_gestureModeClock?.datetime = null;
_gestureMode = GestureMode.browserGestures;
}
AlarmClock? _gestureModeClock;
AlarmClock? _getGestureModeClock() {
@ -2406,6 +2415,12 @@ class EngineSemantics {
'mousemove',
'mouseleave',
'mouseup',
// The wheel event disables browser gestures to allow the framework handle
// the scrolling. Doing otherwise would cause [SemanticScrollable] to send
// [SemanticsAction.scrollUp/Down] to the framework leading to scroll
// position jerks. See https://github.com/flutter/flutter/issues/159358.
'wheel',
];
if (pointerEventTypes.contains(event.type)) {

View File

@ -733,6 +733,21 @@ void testMain() {
expect(event.defaultPrevented, isFalse);
});
test('wheel event - switches semantics to pointer event mode', () async {
EngineSemantics.instance.debugResetGestureMode();
expect(EngineSemantics.instance.gestureMode, GestureMode.browserGestures);
// Synthesize a 'wheel' event.
final DomEvent event = _PointerEventContext().wheel(
buttons: 0,
clientX: 10,
clientY: 10,
deltaX: 10,
deltaY: 0,
);
rootElement.dispatchEvent(event);
expect(EngineSemantics.instance.gestureMode, GestureMode.pointerEvents);
});
test('does synthesize add or hover or move for scroll', () {
final _ButtonedEventMixin context = _PointerEventContext();
final List<ui.PointerDataPacket> packets = <ui.PointerDataPacket>[];

View File

@ -1466,6 +1466,80 @@ void _testVerticalScrolling() {
// Engine semantics returns scroll top back to neutral.
expect(scrollable.scrollTop >= (10 - browserMaxScrollDiff), isTrue);
});
test('scrollable switches to pointer event mode on a wheel event', () async {
final actionLog = <ui.SemanticsActionEvent>[];
ui.PlatformDispatcher.instance.onSemanticsActionEvent = actionLog.add;
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
addTearDown(() async {
semantics().semanticsEnabled = false;
});
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
updateNode(
builder,
actions: 0 | ui.SemanticsAction.scrollUp.index | ui.SemanticsAction.scrollDown.index,
transform: Matrix4.identity().toFloat64(),
rect: const ui.Rect.fromLTRB(0, 0, 50, 100),
childrenInHitTestOrder: Int32List.fromList(<int>[1, 2, 3]),
childrenInTraversalOrder: Int32List.fromList(<int>[1, 2, 3]),
);
for (int id = 1; id <= 3; id++) {
updateNode(
builder,
id: id,
transform: Matrix4.translationValues(0, 50.0 * id, 0).toFloat64(),
rect: const ui.Rect.fromLTRB(0, 0, 50, 50),
);
}
owner().updateSemantics(builder.build());
expectSemanticsTree(owner(), '''
<sem style="touch-action: none; overflow-y: scroll">
<flt-semantics-scroll-overflow></flt-semantics-scroll-overflow>
<sem-c>
<sem style="z-index: 3"></sem>
<sem style="z-index: 2"></sem>
<sem style="z-index: 1"></sem>
</sem-c>
</sem>''');
final DomElement scrollable = owner().debugSemanticsTree![0]!.element;
expect(scrollable, isNotNull);
void expectNeutralPosition() {
// Browsers disagree on the exact value, but it's always close to 10.
expect((scrollable.scrollTop - 10).abs(), lessThan(2));
}
// Initially, starting with a neutral scroll position, everything should be
// in browser gesture mode, react to DOM scroll events, and generate
// semantic actions.
expectNeutralPosition();
expect(semantics().gestureMode, GestureMode.browserGestures);
scrollable.scrollTop = 20;
expect(scrollable.scrollTop, 20);
await Future<void>.delayed(const Duration(milliseconds: 100));
expect(actionLog, hasLength(1));
final capturedEvent = actionLog.removeLast();
expect(capturedEvent.type, ui.SemanticsAction.scrollUp);
// Now, starting with a neutral mode, observing a DOM "wheel" event should
// swap into pointer event mode, and the scrollable becomes a plain clip,
// i.e. `overflow: hidden`.
expectNeutralPosition();
expect(semantics().gestureMode, GestureMode.browserGestures);
expect(scrollable.style.overflowY, 'scroll');
semantics().receiveGlobalEvent(createDomEvent('Event', 'wheel'));
expect(semantics().gestureMode, GestureMode.pointerEvents);
expect(scrollable.style.overflowY, 'hidden');
});
}
void _testHorizontalScrolling() {