Skip to content

Commit b02c436

Browse files
authored
[web] Fix semantic scrollable when there are no scroll actions (#165064)
1 parent 40de991 commit b02c436

File tree

2 files changed

+60
-11
lines changed

2 files changed

+60
-11
lines changed

engine/src/flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:meta/meta.dart';
56
import 'package:ui/src/engine.dart';
67
import 'package:ui/ui.dart' as ui;
78

@@ -55,12 +56,17 @@ class SemanticScrollable extends SemanticRole {
5556
///
5657
/// This gesture is converted to [ui.SemanticsAction.scrollUp] or
5758
/// [ui.SemanticsAction.scrollDown], depending on the direction.
58-
DomEventListener? _scrollListener;
59+
@visibleForTesting
60+
DomEventListener? scrollListener;
5961

6062
/// The value of the "scrollTop" or "scrollLeft" property of this object's
6163
/// [element] that has zero offset relative to the [scrollPosition].
6264
int _effectiveNeutralScrollPosition = 0;
6365

66+
/// Whether this scrollable can scroll vertically or horizontally.
67+
bool get _canScroll =>
68+
semanticsObject.isVerticalScrollContainer || semanticsObject.isHorizontalScrollContainer;
69+
6470
/// Responds to browser-detected "scroll" gestures.
6571
void _recomputeScrollPosition() {
6672
if (_domScrollPosition != _effectiveNeutralScrollPosition) {
@@ -135,7 +141,9 @@ class SemanticScrollable extends SemanticRole {
135141
semanticsObject.updateChildrenPositionAndSize();
136142
});
137143

138-
if (_scrollListener == null) {
144+
_updateCssOverflow();
145+
146+
if (scrollListener == null) {
139147
// We need to set touch-action:none explicitly here, despite the fact
140148
// that we already have it on the <body> tag because overflow:scroll
141149
// still causes the browser to take over pointer events in order to
@@ -146,20 +154,22 @@ class SemanticScrollable extends SemanticRole {
146154
// CSS property. In Safari the `PointerBinding` uses `preventDefault`
147155
// to prevent browser scrolling.
148156
element.style.touchAction = 'none';
149-
_gestureModeDidChange();
150157

151158
// Memoize the tear-off because Dart does not guarantee that two
152159
// tear-offs of a method on the same instance will produce the same
153160
// object.
154161
_gestureModeListener = (_) {
155-
_gestureModeDidChange();
162+
_updateCssOverflow();
156163
};
157164
EngineSemantics.instance.addGestureModeListener(_gestureModeListener!);
158165

159-
_scrollListener = createDomEventListener((_) {
166+
scrollListener = createDomEventListener((_) {
167+
if (!_canScroll) {
168+
return;
169+
}
160170
_recomputeScrollPosition();
161171
});
162-
addEventListener('scroll', _scrollListener);
172+
addEventListener('scroll', scrollListener);
163173
}
164174
}
165175

@@ -207,7 +217,7 @@ class SemanticScrollable extends SemanticRole {
207217
semanticsObject
208218
..verticalScrollAdjustment = _effectiveNeutralScrollPosition.toDouble()
209219
..horizontalScrollAdjustment = 0.0;
210-
} else {
220+
} else if (semanticsObject.isHorizontalScrollContainer) {
211221
// Place the _scrollOverflowElement at the end of the content and
212222
// make sure that when we neutralize the scrolling position,
213223
// it doesn't scroll into the visible area.
@@ -223,10 +233,21 @@ class SemanticScrollable extends SemanticRole {
223233
semanticsObject
224234
..verticalScrollAdjustment = 0.0
225235
..horizontalScrollAdjustment = _effectiveNeutralScrollPosition.toDouble();
236+
} else {
237+
_scrollOverflowElement.style
238+
..transform = 'translate(0px,0px)'
239+
..width = '0px'
240+
..height = '0px';
241+
element.scrollLeft = 0.0;
242+
element.scrollTop = 0.0;
243+
_effectiveNeutralScrollPosition = 0;
244+
semanticsObject
245+
..verticalScrollAdjustment = 0.0
246+
..horizontalScrollAdjustment = 0.0;
226247
}
227248
}
228249

229-
void _gestureModeDidChange() {
250+
void _updateCssOverflow() {
230251
switch (EngineSemantics.instance.gestureMode) {
231252
case GestureMode.browserGestures:
232253
// overflow:scroll will cause the browser report "scroll" events when
@@ -261,9 +282,9 @@ class SemanticScrollable extends SemanticRole {
261282
style.removeProperty('overflowY');
262283
style.removeProperty('overflowX');
263284
style.removeProperty('touch-action');
264-
if (_scrollListener != null) {
265-
removeEventListener('scroll', _scrollListener);
266-
_scrollListener = null;
285+
if (scrollListener != null) {
286+
removeEventListener('scroll', scrollListener);
287+
scrollListener = null;
267288
}
268289
if (_gestureModeListener != null) {
269290
EngineSemantics.instance.removeGestureModeListener(_gestureModeListener!);

engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:js_interop';
67
import 'dart:typed_data';
78

89
import 'package:quiver/testing/async.dart';
@@ -1560,6 +1561,33 @@ void _testVerticalScrolling() {
15601561
semantics().semanticsEnabled = false;
15611562
});
15621563

1564+
test('scroll events ignored when actions not available', () async {
1565+
semantics()
1566+
..debugOverrideTimestampFunction(() => _testTime)
1567+
..semanticsEnabled = true;
1568+
1569+
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
1570+
updateNode(
1571+
builder,
1572+
flags: 0 | ui.SemanticsFlag.hasImplicitScrolling.index,
1573+
transform: Matrix4.identity().toFloat64(),
1574+
rect: const ui.Rect.fromLTRB(0, 0, 50, 100),
1575+
);
1576+
1577+
owner().updateSemantics(builder.build());
1578+
expectSemanticsTree(owner(), '''
1579+
<sem role="group" style="touch-action: none">
1580+
<flt-semantics-scroll-overflow></flt-semantics-scroll-overflow>
1581+
</sem>''');
1582+
1583+
final scrollable = owner().debugSemanticsTree![0]!.semanticRole! as SemanticScrollable;
1584+
final scrollEvent = createDomEvent('Event', 'scroll') as JSAny;
1585+
final listener = scrollable.scrollListener! as JSFunction;
1586+
expect(() => listener.callAsFunction(null, scrollEvent), returnsNormally);
1587+
1588+
semantics().semanticsEnabled = false;
1589+
});
1590+
15631591
test('renders an empty scrollable node', () async {
15641592
semantics()
15651593
..debugOverrideTimestampFunction(() => _testTime)

0 commit comments

Comments
 (0)