This is an automated email from the ASF dual-hosted git repository.
hugoferreira pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/royale-asjs.git
The following commit(s) were added to refs/heads/develop by this push:
new 5b91a7e4a9 Fix VirtualDataGrid column misalignment during scroll
5b91a7e4a9 is described below
commit 5b91a7e4a9517a863e3b527ceefc839521892835
Author: Hugo Ferreira <[email protected]>
AuthorDate: Wed Apr 1 21:50:23 2026 +0100
Fix VirtualDataGrid column misalignment during scroll
Two related but distinct causes were addressed in
VirtualDataGridListAreaLayout:
1. Misalignment during scroll (compositor race)
Modern browsers use a compositor thread to scroll elements visually before
the JavaScript main thread has a chance to run. In a VirtualDataGrid, this
meant the column under the mouse was moved ahead by the compositor while the
other columns were still waiting for the cross-column scrollTop sync in
scrollHandler. The result was one or more frames of visible misalignment while
scrolling.
Fixed by adding a non-passive wheel event listener to each column element.
Calling e.preventDefault() inside it blocks the compositor from scrolling the
column natively. Scrolling is then driven entirely from JavaScript — setting
scrollTop manually triggers the existing scrollHandler, which propagates the
position to all sibling columns before the browser paints.
2. Misalignment after fast scroll (async layout race)
During fast scrolling, the focused column's scrollHandler would set
scrollTop on sibling columns synchronously, but those siblings only called
layout() when their own queued scroll events eventually fired — potentially in
a later frame. This left a window where the focused column had already painted
its new item renderers while the others still showed stale content.
Fixed by calling layout() on each sibling column immediately after setting
its scrollTop, within the same execution context as the focused column's sync
loop. A static _syncingDataGrid flag suppresses the subsequent redundant
layout() calls that would otherwise arrive from the queued scroll events.
---
.../beads/layouts/VirtualDataGridListAreaLayout.as | 79 ++++++++++++++++++----
1 file changed, 67 insertions(+), 12 deletions(-)
diff --git
a/frameworks/projects/Jewel/src/main/royale/org/apache/royale/jewel/beads/layouts/VirtualDataGridListAreaLayout.as
b/frameworks/projects/Jewel/src/main/royale/org/apache/royale/jewel/beads/layouts/VirtualDataGridListAreaLayout.as
index 8304bc9ff6..751b2eb060 100644
---
a/frameworks/projects/Jewel/src/main/royale/org/apache/royale/jewel/beads/layouts/VirtualDataGridListAreaLayout.as
+++
b/frameworks/projects/Jewel/src/main/royale/org/apache/royale/jewel/beads/layouts/VirtualDataGridListAreaLayout.as
@@ -22,6 +22,7 @@ package org.apache.royale.jewel.beads.layouts
import org.apache.royale.html.beads.IDataGridView;
import org.apache.royale.core.IStrand;
import org.apache.royale.core.IDataGrid;
+ import org.apache.royale.core.IBeadLayout;
import org.apache.royale.jewel.Container;
import org.apache.royale.jewel.supportClasses.datagrid.IDataGridColumnList;
@@ -37,6 +38,10 @@ package org.apache.royale.jewel.beads.layouts
{
private var isAreaFocus:Boolean;
+ // Tracks which DataGrid is currently syncing columns, to suppress
redundant
+ // layout() calls from scroll events triggered by programmatic
scrollTop changes.
+ private static var _syncingDataGrid:IDataGrid = null;
+
/**
* Constructor.
*
@@ -59,9 +64,31 @@ package org.apache.royale.jewel.beads.layouts
{
host.element.addEventListener("mouseover", function():void {
isAreaFocus = true });
host.element.addEventListener("mouseleave", function():void {
isAreaFocus = false });
+
+ // Non-passive wheel listener so preventDefault() can block
the browser's
+ // compositor thread from scrolling this column ahead of its
siblings.
+ // Without this, the compositor moves this column visually
before our
+ // scrollHandler fires, producing one frame of cross-column
misalignment.
+ var wheelOptions:Object = new Object();
+ wheelOptions["passive"] = false;
+ host.element.addEventListener("wheel", wheelHandler,
wheelOptions);
}
}
+ COMPILE::JS
+ private function wheelHandler(e:*):void
+ {
+ if (!isAreaFocus || e.deltaY == 0) return;
+
+ e.preventDefault();
+
+ // deltaMode: 0 = pixels (trackpad/modern), 1 = lines (mouse wheel
on Firefox)
+ var delta:Number = (e.deltaMode == 0) ? e.deltaY : e.deltaY * 40;
+ var limitY:Number = host.element.scrollHeight -
host.element.clientHeight;
+ host.element.scrollTop = Math.max(0,
Math.min(host.element.scrollTop + delta, limitY));
+ // The resulting scroll event triggers scrollHandler which syncs
all other columns.
+ }
+
private function getListArea():Container
{
var datagrid:IDataGrid = (host as IDataGridColumnList).datagrid;
@@ -71,23 +98,51 @@ package org.apache.royale.jewel.beads.layouts
override protected function scrollHandler(e:Event):void
{
- super.scrollHandler(e);
-
- //this is not ideal, but avoids that the other VirtualDataGrid
columns
- //also try to do scrollTop to all columns again in a loop, that
causes a performance issue
- //and a strang behaviour on the first column that started the
process
if (!isAreaFocus)
+ {
+ // Non-focused column: skip layout() if the focused column
already synced us
+ // and called layout() directly — avoids a redundant call
before the browser paints.
+ COMPILE::JS
+ {
+ if (_syncingDataGrid != null && _syncingDataGrid == (host
as IDataGridColumnList).datagrid)
+ return;
+ }
+ super.scrollHandler(e);
return;
+ }
- var listArea:Container = getListArea();
+ // Focused column: update its own layout first.
+ super.scrollHandler(e);
- for (var i:int = 0; i < listArea.numElements; i++)
+ COMPILE::JS
{
- COMPILE::JS
- {
- if (listArea.getElementAt(i) != host)
- listArea.getElementAt(i).element.scrollTop =
host.element.scrollTop;
- }
+ var myDataGrid:IDataGrid = (host as
IDataGridColumnList).datagrid;
+ _syncingDataGrid = myDataGrid;
+
+ var listArea:Container = getListArea();
+ var scrollTop:Number = host.element.scrollTop;
+
+ for (var i:int = 0; i < listArea.numElements; i++)
+ {
+ var otherColumn:* = listArea.getElementAt(i);
+ if (otherColumn != host)
+ {
+ otherColumn.element.scrollTop = scrollTop;
+ // Immediately call layout() so all columns are
visually aligned
+ // before the browser paints — without waiting for the
async scroll event.
+ var otherLayout:IBeadLayout =
otherColumn.getBeadByType(IBeadLayout) as IBeadLayout;
+ if (otherLayout)
+ otherLayout.layout();
+ }
+ }
+
+ // Clear the flag after any queued scroll events from the
programmatic
+ // scrollTop changes above have been processed and suppressed.
+ setTimeout(function():void
+ {
+ if (_syncingDataGrid == myDataGrid)
+ _syncingDataGrid = null;
+ }, 0);
}
}
}