From 413fee40797b7356856ebe9c57a9e60433d86c95 Mon Sep 17 00:00:00 2001
From: pupiltong <12288479+PupilTong@users.noreply.github.com>
Date: Tue, 27 Jan 2026 17:43:03 +0800
Subject: [PATCH 1/4] feat: add wheel event handling and corresponding tests
for x-foldview-ng
---
.changeset/nasty-clouds-win.md | 5 +
.../XFoldviewSlotNgTouchEventsHandler.ts | 109 +++++++++++++-----
.../x-foldview-ng/wheel-parent-first.html | 67 +++++++++++
.../x-foldview-ng/wheel-smooth-continue.html | 69 +++++++++++
.../tests/x-foldview-ng-wheel.spec.ts | 93 +++++++++++++++
5 files changed, 317 insertions(+), 26 deletions(-)
create mode 100644 .changeset/nasty-clouds-win.md
create mode 100644 packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/wheel-parent-first.html
create mode 100644 packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/wheel-smooth-continue.html
create mode 100644 packages/web-platform/web-elements/tests/x-foldview-ng-wheel.spec.ts
diff --git a/.changeset/nasty-clouds-win.md b/.changeset/nasty-clouds-win.md
new file mode 100644
index 0000000000..89b0eff9bf
--- /dev/null
+++ b/.changeset/nasty-clouds-win.md
@@ -0,0 +1,5 @@
+---
+"@lynx-js/web-elements": patch
+---
+
+feat: add wheel event handling and corresponding tests for x-foldview-ng
diff --git a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts
index 6dcebe9dec..24e03359b6 100644
--- a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts
+++ b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts
@@ -32,6 +32,9 @@ export class XFoldviewSlotNgTouchEventsHandler
this.#dom.addEventListener('touchend', this.#touchEnd, {
passive: true,
});
+ this.#dom.addEventListener('wheel', this.#handleWheel, {
+ passive: false,
+ });
}
#getTheMostScrollableKid(delta: number) {
@@ -63,6 +66,9 @@ export class XFoldviewSlotNgTouchEventsHandler
#scroller = (event: TouchEvent) => {
const parentElement = this.#getParentElement();
+ if (!parentElement) {
+ return;
+ }
const touch = event.touches.item(0)!;
const { pageY, pageX } = touch;
const deltaY = this.#previousPageY! - pageY;
@@ -73,37 +79,45 @@ export class XFoldviewSlotNgTouchEventsHandler
if (this.#scrollingVertically === false) {
return;
}
- const scrollableKidY = this.#getTheMostScrollableKid(deltaY);
- if (
- parentElement
- ) {
- if (event.cancelable) {
- event.preventDefault();
- }
- if (
- (parentElement[isHeaderShowing] && deltaY > 0
- || (deltaY < 0 && !scrollableKidY))
- // deltaY > 0: swipe up (folding header)
- // scroll the foldview if its scrollable
- || (!parentElement[isHeaderShowing] && !scrollableKidY)
- // all sub doms are scrolled
- ) {
- parentElement.scrollBy({
- top: deltaY,
- behavior: 'smooth',
- });
- this.#parentScrollTop += deltaY;
- parentElement.scrollTop = this.#parentScrollTop;
- this.#currentScrollingElement = parentElement;
- } else if (scrollableKidY) {
- this.#currentScrollingElement = scrollableKidY;
- this.#scrollKid(scrollableKidY, deltaY);
- }
+ if (event.cancelable) {
+ event.preventDefault();
}
+ this.#handleScrollDelta(deltaY, parentElement, true, false);
this.#previousPageY = pageY;
this.#deltaY = deltaY;
};
+ #handleWheel = (event: WheelEvent) => {
+ const parentElement = this.#getParentElement();
+ if (!parentElement) {
+ return;
+ }
+ if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) {
+ return;
+ }
+ const pathElements = event.composedPath().filter((
+ element,
+ ): element is Element =>
+ element instanceof Element && this.#dom.contains(element)
+ );
+ const { clientX, clientY } = event;
+ const pointElements = document.elementsFromPoint(clientX, clientY).filter(
+ e => this.#dom.contains(e),
+ );
+ this.#elements = [...new Set([...pathElements, ...pointElements])];
+ this.#parentScrollTop = parentElement.scrollTop;
+ if (this.#elements) {
+ for (const element of this.#elements) {
+ this.#childrenElemsntsScrollTop.set(element, element.scrollTop);
+ }
+ }
+ if (event.cancelable) {
+ event.preventDefault();
+ }
+ this.#handleScrollDelta(event.deltaY, parentElement, true, true);
+ this.#deltaY = event.deltaY;
+ };
+
#getParentElement(): XFoldviewNg | void {
const parentElement = this.#dom.parentElement;
if (parentElement && parentElement.tagName === 'X-FOLDVIEW-NG') {
@@ -142,4 +156,47 @@ export class XFoldviewSlotNgTouchEventsHandler
});
}
};
+
+ #handleScrollDelta(
+ deltaY: number,
+ parentElement: XFoldviewNg,
+ smoothParent: boolean,
+ allowRemainderToChild: boolean,
+ ) {
+ if (deltaY === 0) {
+ return;
+ }
+ const scrollableKidY = this.#getTheMostScrollableKid(deltaY);
+ if (
+ (parentElement[isHeaderShowing] && deltaY > 0
+ || (deltaY < 0 && !scrollableKidY))
+ // deltaY > 0: swipe up (folding header)
+ // scroll the foldview if its scrollable
+ || (!parentElement[isHeaderShowing] && !scrollableKidY)
+ // all sub doms are scrolled
+ ) {
+ const previousScrollTop = this.#parentScrollTop;
+ this.#parentScrollTop += deltaY;
+ parentElement.scrollTop = this.#parentScrollTop;
+ const appliedDelta = parentElement.scrollTop - previousScrollTop;
+ this.#parentScrollTop = parentElement.scrollTop;
+ if (smoothParent && appliedDelta !== 0) {
+ parentElement.scrollBy({
+ top: appliedDelta,
+ behavior: 'smooth',
+ });
+ }
+ this.#currentScrollingElement = parentElement;
+ if (allowRemainderToChild) {
+ const remainingDelta = deltaY - appliedDelta;
+ if (remainingDelta !== 0 && scrollableKidY) {
+ this.#currentScrollingElement = scrollableKidY;
+ this.#scrollKid(scrollableKidY, remainingDelta);
+ }
+ }
+ } else if (scrollableKidY) {
+ this.#currentScrollingElement = scrollableKidY;
+ this.#scrollKid(scrollableKidY, deltaY);
+ }
+ }
}
diff --git a/packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/wheel-parent-first.html b/packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/wheel-parent-first.html
new file mode 100644
index 0000000000..7c720bf9ee
--- /dev/null
+++ b/packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/wheel-parent-first.html
@@ -0,0 +1,67 @@
+
+
+
+
+
+ web playground
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/wheel-smooth-continue.html b/packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/wheel-smooth-continue.html
new file mode 100644
index 0000000000..669e7bc870
--- /dev/null
+++ b/packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/wheel-smooth-continue.html
@@ -0,0 +1,69 @@
+
+
+
+
+
+ web playground
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/web-platform/web-elements/tests/x-foldview-ng-wheel.spec.ts b/packages/web-platform/web-elements/tests/x-foldview-ng-wheel.spec.ts
new file mode 100644
index 0000000000..83d16a45c4
--- /dev/null
+++ b/packages/web-platform/web-elements/tests/x-foldview-ng-wheel.spec.ts
@@ -0,0 +1,93 @@
+// Copyright 2024 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+import { test, expect } from '@lynx-js/playwright-fixtures';
+import type { Page } from '@playwright/test';
+
+const wait = async (ms: number) => {
+ await new Promise((resolve) => {
+ setTimeout(resolve, ms);
+ });
+};
+
+const goto = async (page: Page, fixtureName: string) => {
+ await page.goto(`/tests/fixtures/${fixtureName}.html`, {
+ waitUntil: 'load',
+ });
+ await page.evaluate(() => document.fonts.ready);
+};
+
+test.describe('x-foldview-ng wheel', () => {
+ test('x-foldview-ng/wheel-parent-first', async ({ page, browserName }, {
+ title,
+ }) => {
+ test.skip(browserName === 'webkit', 'mouse wheel unsupported on webkit');
+ await goto(page, title);
+ const foldview = page.locator('#foldview');
+ const scrollview = page.locator('#inner-scroll');
+ await page.locator('#inner-scroll').hover();
+
+ await foldview.evaluate((dom: HTMLElement) => {
+ dom.scrollTop = 0;
+ });
+ await scrollview.evaluate((dom: HTMLElement) => {
+ dom.scrollTop = 0;
+ });
+
+ const foldviewInitial = await foldview.evaluate((dom: HTMLElement) =>
+ dom.scrollTop
+ );
+ const scrollViewInitial = await scrollview.evaluate((dom: HTMLElement) =>
+ dom.scrollTop
+ );
+
+ await page.mouse.wheel(0, 120);
+ await wait(200);
+
+ expect(
+ await foldview.evaluate((dom: HTMLElement) => dom.scrollTop),
+ 'wheel-outer-scrolls-first',
+ ).toBeGreaterThan(foldviewInitial);
+ expect(
+ await scrollview.evaluate((dom: HTMLElement) => dom.scrollTop),
+ 'wheel-inner-not-scrolled-before-header',
+ ).toBe(scrollViewInitial);
+ });
+
+ test('x-foldview-ng/wheel-smooth-continue', async ({ page, browserName }, {
+ title,
+ }) => {
+ test.skip(browserName === 'webkit', 'mouse wheel unsupported on webkit');
+ await goto(page, title);
+ const foldview = page.locator('#foldview');
+ const scrollview = page.locator('#inner-scroll');
+ await page.locator('#inner-scroll').hover();
+
+ await foldview.evaluate((dom: HTMLElement) => {
+ dom.scrollTop = 0;
+ });
+ await scrollview.evaluate((dom: HTMLElement) => {
+ dom.scrollTop = 0;
+ });
+
+ await foldview.evaluate((dom: HTMLElement) => {
+ dom.scrollTop = dom.scrollHeight;
+ });
+ await wait(100);
+
+ const foldviewBeforeInner = await foldview.evaluate((dom: HTMLElement) =>
+ dom.scrollTop
+ );
+ await page.mouse.wheel(0, 200);
+ await wait(200);
+
+ expect(
+ await scrollview.evaluate((dom: HTMLElement) => dom.scrollTop),
+ 'wheel-continues-to-inner-scroll',
+ ).toBeGreaterThan(0);
+ expect(
+ await foldview.evaluate((dom: HTMLElement) => dom.scrollTop),
+ 'wheel-outer-stays-at-end',
+ ).toBeGreaterThanOrEqual(foldviewBeforeInner);
+ });
+});
From f2cd0c613faf352ff2faa9220527952b2c05a9de Mon Sep 17 00:00:00 2001
From: pupiltong <12288479+PupilTong@users.noreply.github.com>
Date: Tue, 27 Jan 2026 18:07:22 +0800
Subject: [PATCH 2/4] + update var name for ai comment
---
.../XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts
index 24e03359b6..2ff5531464 100644
--- a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts
+++ b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts
@@ -178,17 +178,17 @@ export class XFoldviewSlotNgTouchEventsHandler
const previousScrollTop = this.#parentScrollTop;
this.#parentScrollTop += deltaY;
parentElement.scrollTop = this.#parentScrollTop;
- const appliedDelta = parentElement.scrollTop - previousScrollTop;
+ const flingDelta = parentElement.scrollTop - previousScrollTop;
this.#parentScrollTop = parentElement.scrollTop;
- if (smoothParent && appliedDelta !== 0) {
+ if (smoothParent && flingDelta !== 0) {
parentElement.scrollBy({
- top: appliedDelta,
+ top: flingDelta,
behavior: 'smooth',
});
}
this.#currentScrollingElement = parentElement;
if (allowRemainderToChild) {
- const remainingDelta = deltaY - appliedDelta;
+ const remainingDelta = deltaY - flingDelta;
if (remainingDelta !== 0 && scrollableKidY) {
this.#currentScrollingElement = scrollableKidY;
this.#scrollKid(scrollableKidY, remainingDelta);
From f46d9f162aaf5221007ee52d1c175408753f01c7 Mon Sep 17 00:00:00 2001
From: pupiltong <12288479+PupilTong@users.noreply.github.com>
Date: Tue, 27 Jan 2026 18:25:21 +0800
Subject: [PATCH 3/4] feat: enhance scrolling behavior in
XFoldviewSlotNgTouchEventsHandler for smoother parent scrolling
---
.../XFoldviewSlotNgTouchEventsHandler.ts | 20 +++++++++++++------
1 file changed, 14 insertions(+), 6 deletions(-)
diff --git a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts
index 2ff5531464..fc70502976 100644
--- a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts
+++ b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts
@@ -175,6 +175,16 @@ export class XFoldviewSlotNgTouchEventsHandler
|| (!parentElement[isHeaderShowing] && !scrollableKidY)
// all sub doms are scrolled
) {
+ if (!allowRemainderToChild) {
+ parentElement.scrollBy({
+ top: deltaY,
+ behavior: smoothParent ? 'smooth' : 'auto',
+ });
+ this.#parentScrollTop += deltaY;
+ parentElement.scrollTop = this.#parentScrollTop;
+ this.#currentScrollingElement = parentElement;
+ return;
+ }
const previousScrollTop = this.#parentScrollTop;
this.#parentScrollTop += deltaY;
parentElement.scrollTop = this.#parentScrollTop;
@@ -187,12 +197,10 @@ export class XFoldviewSlotNgTouchEventsHandler
});
}
this.#currentScrollingElement = parentElement;
- if (allowRemainderToChild) {
- const remainingDelta = deltaY - flingDelta;
- if (remainingDelta !== 0 && scrollableKidY) {
- this.#currentScrollingElement = scrollableKidY;
- this.#scrollKid(scrollableKidY, remainingDelta);
- }
+ const remainingDelta = deltaY - flingDelta;
+ if (remainingDelta !== 0 && scrollableKidY) {
+ this.#currentScrollingElement = scrollableKidY;
+ this.#scrollKid(scrollableKidY, remainingDelta);
}
} else if (scrollableKidY) {
this.#currentScrollingElement = scrollableKidY;
From c4761b417ffc2debf5eb9f7022bd11a814b2fdbd Mon Sep 17 00:00:00 2001
From: pupiltong <12288479+PupilTong@users.noreply.github.com>
Date: Tue, 27 Jan 2026 19:29:21 +0800
Subject: [PATCH 4/4] + human reviewed
---
.../XFoldviewSlotNgTouchEventsHandler.ts | 44 ++++---------------
1 file changed, 9 insertions(+), 35 deletions(-)
diff --git a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts
index fc70502976..2e2cfcba33 100644
--- a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts
+++ b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts
@@ -22,7 +22,7 @@ export class XFoldviewSlotNgTouchEventsHandler
constructor(dom: XFoldviewSlotNg) {
this.#dom = dom;
- this.#dom.addEventListener('touchmove', this.#scroller, {
+ this.#dom.addEventListener('touchmove', this.#handleTouch, {
passive: false,
});
@@ -64,7 +64,7 @@ export class XFoldviewSlotNgTouchEventsHandler
scrollableKid.scrollTop = targetKidScrollDistance;
}
- #scroller = (event: TouchEvent) => {
+ #handleTouch = (event: TouchEvent) => {
const parentElement = this.#getParentElement();
if (!parentElement) {
return;
@@ -82,9 +82,8 @@ export class XFoldviewSlotNgTouchEventsHandler
if (event.cancelable) {
event.preventDefault();
}
- this.#handleScrollDelta(deltaY, parentElement, true, false);
+ this.#handleScrollDelta(deltaY, parentElement);
this.#previousPageY = pageY;
- this.#deltaY = deltaY;
};
#handleWheel = (event: WheelEvent) => {
@@ -114,8 +113,7 @@ export class XFoldviewSlotNgTouchEventsHandler
if (event.cancelable) {
event.preventDefault();
}
- this.#handleScrollDelta(event.deltaY, parentElement, true, true);
- this.#deltaY = event.deltaY;
+ this.#handleScrollDelta(event.deltaY, parentElement);
};
#getParentElement(): XFoldviewNg | void {
@@ -160,12 +158,7 @@ export class XFoldviewSlotNgTouchEventsHandler
#handleScrollDelta(
deltaY: number,
parentElement: XFoldviewNg,
- smoothParent: boolean,
- allowRemainderToChild: boolean,
) {
- if (deltaY === 0) {
- return;
- }
const scrollableKidY = this.#getTheMostScrollableKid(deltaY);
if (
(parentElement[isHeaderShowing] && deltaY > 0
@@ -175,36 +168,17 @@ export class XFoldviewSlotNgTouchEventsHandler
|| (!parentElement[isHeaderShowing] && !scrollableKidY)
// all sub doms are scrolled
) {
- if (!allowRemainderToChild) {
- parentElement.scrollBy({
- top: deltaY,
- behavior: smoothParent ? 'smooth' : 'auto',
- });
- this.#parentScrollTop += deltaY;
- parentElement.scrollTop = this.#parentScrollTop;
- this.#currentScrollingElement = parentElement;
- return;
- }
- const previousScrollTop = this.#parentScrollTop;
+ parentElement.scrollBy({
+ top: deltaY,
+ behavior: 'smooth',
+ });
this.#parentScrollTop += deltaY;
parentElement.scrollTop = this.#parentScrollTop;
- const flingDelta = parentElement.scrollTop - previousScrollTop;
- this.#parentScrollTop = parentElement.scrollTop;
- if (smoothParent && flingDelta !== 0) {
- parentElement.scrollBy({
- top: flingDelta,
- behavior: 'smooth',
- });
- }
this.#currentScrollingElement = parentElement;
- const remainingDelta = deltaY - flingDelta;
- if (remainingDelta !== 0 && scrollableKidY) {
- this.#currentScrollingElement = scrollableKidY;
- this.#scrollKid(scrollableKidY, remainingDelta);
- }
} else if (scrollableKidY) {
this.#currentScrollingElement = scrollableKidY;
this.#scrollKid(scrollableKidY, deltaY);
}
+ this.#deltaY = deltaY;
}
}