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; } }