From 0014d214d6727b0da1d5fda9235ab7f328d8693b Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:08:01 +0800 Subject: [PATCH 1/3] feat: reimplement XFoldViewNg scrolling using CSS transforms and custom scroll handling, updating related events, styles, and tests. --- .../web-elements/rsbuild.config.ts | 1 + .../elements/XFoldViewNg/XFoldviewHeaderNg.ts | 40 ++++--- .../src/elements/XFoldViewNg/XFoldviewNg.ts | 105 +++++++++++++----- .../elements/XFoldViewNg/XFoldviewNgEvents.ts | 6 +- ...er.ts => XFoldviewNgTouchEventsHandler.ts} | 69 ++++++------ .../elements/XFoldViewNg/XFoldviewSlotNg.ts | 8 -- .../XFoldViewNg/XFoldviewToolbarNg.ts | 40 ++++--- .../elements/XFoldViewNg/x-foldview-ng.css | 20 ++-- .../fixtures/x-foldview-ng/item-fixed.html | 2 +- .../web-elements/tests/web-elements.spec.ts | 9 +- .../index-chromium-linux.png | Bin 2524 -> 2498 bytes .../index-firefox-linux.png | Bin 21017 -> 20050 bytes .../index-webkit-linux.png | Bin 2380 -> 2340 bytes .../300px-inf-webkit-linux.png | Bin 0 -> 2432 bytes .../tests/x-foldview-ng-wheel.spec.ts | 60 +++++----- 15 files changed, 215 insertions(+), 145 deletions(-) rename packages/web-platform/web-elements/src/elements/XFoldViewNg/{XFoldviewSlotNgTouchEventsHandler.ts => XFoldviewNgTouchEventsHandler.ts} (76%) create mode 100644 packages/web-platform/web-elements/tests/web-elements.spec.ts-snapshots/x-foldview-ng/size-parent-grow-children-specific/300px-inf-webkit-linux.png diff --git a/packages/web-platform/web-elements/rsbuild.config.ts b/packages/web-platform/web-elements/rsbuild.config.ts index 1e9cc15e9b..5515734369 100644 --- a/packages/web-platform/web-elements/rsbuild.config.ts +++ b/packages/web-platform/web-elements/rsbuild.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ output: { assetPrefix: 'auto', polyfill: 'off', + overrideBrowserslist: ['last 1 Chrome versions'], distPath: { root: 'www', css: '.', diff --git a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewHeaderNg.ts b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewHeaderNg.ts index d6cd7cac66..65db470baa 100644 --- a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewHeaderNg.ts +++ b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewHeaderNg.ts @@ -5,9 +5,8 @@ */ import { Component } from '../../element-reactive/index.js'; import { CommonEventsAndMethods } from '../common/CommonEventsAndMethods.js'; -import { resizeObserver, type XFoldviewNg } from './XFoldviewNg.js'; -import { getCombinedDirectParentElement } from '../common/getCombinedParentElement.js'; import { LinearContainer } from '../../compat/index.js'; +import { updateHeaderHeight, XFoldviewNg } from './XFoldviewNg.js'; @Component( 'x-foldview-header-ng', @@ -17,19 +16,34 @@ import { LinearContainer } from '../../compat/index.js'; ], ) export class XFoldviewHeaderNg extends HTMLElement { - #parentResizeObserver: ResizeObserver | undefined = undefined; + #resizeObserver?: ResizeObserver; + connectedCallback() { - const parentElement = getCombinedDirectParentElement( - this, - 'X-FOLDVIEW-NG', - ); - this.#parentResizeObserver = parentElement?.[resizeObserver]; - this.#parentResizeObserver?.observe(this); + this.#resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + this.#updateParent(entry.contentRect.height); + } + }); + this.#resizeObserver.observe(this); + this.#updateParent(this.clientHeight); + } + + disconnectedCallback() { + this.#resizeObserver?.disconnect(); + this.#resizeObserver = undefined; } - dispose() { - this.#parentResizeObserver?.unobserve( - this, - ); + #updateParent(height: number) { + let parent = this.parentElement; + while (parent) { + if (parent instanceof XFoldviewNg) { + parent[updateHeaderHeight](height); + break; + } + if (parent.tagName !== 'LYNX-WRAPPER') { + break; + } + parent = parent.parentElement; + } } } diff --git a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNg.ts b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNg.ts index 10f4482bd8..1fab566c97 100644 --- a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNg.ts +++ b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNg.ts @@ -6,57 +6,111 @@ import { Component } from '../../element-reactive/index.js'; import { CommonEventsAndMethods } from '../common/CommonEventsAndMethods.js'; import { XFoldviewNgEvents } from './XFoldviewNgEvents.js'; +import { XFoldviewNgTouchEventsHandler } from './XFoldviewNgTouchEventsHandler.js'; import { scrollContainerDom } from '../common/constants.js'; -import type { XFoldviewSlotNg } from './XFoldviewSlotNg.js'; import { LinearContainer } from '../../compat/index.js'; -export const scrollableLength = Symbol('scrollableLength'); export const isHeaderShowing = Symbol('isHeaderShowing'); -export const resizeObserver = Symbol('resizeObserver'); -export const slotKid = Symbol('slotKid'); +export const updateHeaderHeight = Symbol('updateHeaderHeight'); +export const updateToolbarHeight = Symbol('updateToolbarHeight'); +export const scrollCallbacks = Symbol('scrollCallbacks'); @Component('x-foldview-ng', [ LinearContainer, CommonEventsAndMethods, XFoldviewNgEvents, + XFoldviewNgTouchEventsHandler, ]) export class XFoldviewNg extends HTMLElement { static readonly notToFilterFalseAttributes = new Set(['scroll-enable']); - [slotKid]?: XFoldviewSlotNg; - [resizeObserver]?: ResizeObserver = new ResizeObserver((resizeEntries) => { - for (const resize of resizeEntries) { - if (resize.target.tagName === 'X-FOLDVIEW-HEADER-NG') { - this.#headerHeight = resize.contentRect.height; - } else if (resize.target.tagName === 'X-FOLDVIEW-TOOLBAR-NG') { - this.#toolbarHeight = resize.contentRect.height; - } - } - if (this[slotKid]) { - this[slotKid].style.top = `${this.#headerHeight - this.#toolbarHeight}px`; - } - }); #headerHeight: number = 0; #toolbarHeight: number = 0; + [scrollCallbacks]: Set<() => void> = new Set(); - get [scrollableLength](): number { + [updateHeaderHeight](height: number) { + this.#headerHeight = height; + this.style.setProperty( + '--foldview-scroll-height', + this.scrollHeight + 'px', + ); + } + + [updateToolbarHeight](height: number) { + this.#toolbarHeight = height; + this.style.setProperty( + '--foldview-scroll-height', + this.scrollHeight + 'px', + ); + } + + override get scrollHeight(): number { return this.#headerHeight - this.#toolbarHeight; } get [isHeaderShowing](): boolean { // This behavior cannot be reproduced in the current test, but can be reproduced in Android WebView - return this[scrollableLength] - this.scrollTop >= 1; + return this.scrollHeight - this.scrollTop >= 1; + } + + get scrollableLength(): number { + return this.scrollHeight; } + #scrollTop: number = 0; + override get scrollTop() { - return super.scrollTop; + return this.#scrollTop; } override set scrollTop(value: number) { - if (value > this[scrollableLength]) { - value = this[scrollableLength]; + if (value > this.scrollHeight) { + value = this.scrollHeight; } else if (value < 0) { value = 0; } - super.scrollTop = value; + if (this.#scrollTop === value) { + return; + } + this.#scrollTop = value; + this.style.setProperty( + '--foldview-scroll-top', + (0 - value).toString() + 'px', + ); + this.dispatchEvent(new Event('scroll')); + for (const callback of this[scrollCallbacks]) { + callback(); + } + } + + override scrollTo(options?: ScrollToOptions): void; + override scrollTo(x: number, y: number): void; + override scrollTo(arg1?: any, arg2?: any): void { + if (typeof arg1 === 'object') { + const { top, behavior } = arg1; + if (typeof top === 'number') { + if (behavior === 'smooth') { + // TODO: implement smooth scroll if needed, for now just instant + this.scrollTop = top; + } else { + this.scrollTop = top; + } + } + } else if (typeof arg2 === 'number') { + this.scrollTop = arg2; + } + } + + override scrollBy(options?: ScrollToOptions): void; + override scrollBy(x: number, y: number): void; + override scrollBy(arg1?: any, arg2?: any): void { + if (typeof arg1 === 'object') { + const { top, behavior } = arg1; + this.scrollTo({ + top: (typeof top === 'number' ? top : 0) + this.scrollTop, + behavior, + }); + } else { + this.scrollTo(0, this.scrollTop + (arg2 || 0)); + } } setFoldExpanded(params: { offset: string; smooth: boolean }) { @@ -71,9 +125,4 @@ export class XFoldviewNg extends HTMLElement { get [scrollContainerDom]() { return this; } - - disconnectedCallback() { - this[resizeObserver]?.disconnect(); - this[resizeObserver] = undefined; - } } diff --git a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNgEvents.ts b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNgEvents.ts index 66233f6ae6..155cbc7e34 100644 --- a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNgEvents.ts +++ b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNgEvents.ts @@ -9,7 +9,7 @@ import { registerEventEnableStatusChangeHandler, } from '../../element-reactive/index.js'; import { commonComponentEventSetting } from '../common/commonEventInitConfiguration.js'; -import { scrollableLength, type XFoldviewNg } from './XFoldviewNg.js'; +import { type XFoldviewNg } from './XFoldviewNg.js'; export class XFoldviewNgEvents implements InstanceType> @@ -46,7 +46,7 @@ export class XFoldviewNgEvents scrollLength > this.#granularity || this.#dom.scrollTop === 0 || Math.abs( - this.#dom.scrollHeight - this.#dom.clientHeight - this.#dom.scrollTop, + this.#dom.scrollHeight - this.#dom.scrollTop, ) <= 1 ) { this.#pervScroll = currentScrollTop; @@ -55,7 +55,7 @@ export class XFoldviewNgEvents ...commonComponentEventSetting, detail: { offset: currentScrollTop, - height: this.#dom[scrollableLength], + height: this.#dom.scrollableLength, }, }), ); diff --git a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNgTouchEventsHandler.ts similarity index 76% rename from packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts rename to packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNgTouchEventsHandler.ts index 416bdcb33b..76f82af24d 100644 --- a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNgTouchEventsHandler.ts +++ b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNgTouchEventsHandler.ts @@ -5,11 +5,9 @@ */ import type { AttributeReactiveClass } from '../../element-reactive/index.js'; import { isHeaderShowing, type XFoldviewNg } from './XFoldviewNg.js'; -import type { XFoldviewSlotNg } from './XFoldviewSlotNg.js'; -export class XFoldviewSlotNgTouchEventsHandler - implements InstanceType> +export class XFoldviewNgTouchEventsHandler + implements InstanceType> { - #parentScrollTop: number = 0; #childrenElemsntsScrollTop: WeakMap = new WeakMap(); #elements?: Element[]; #previousPageY: number = 0; @@ -17,9 +15,9 @@ export class XFoldviewSlotNgTouchEventsHandler #scrollingVertically: boolean | null = null; #currentScrollingElement?: Element; #deltaY: number = 0; - #dom: XFoldviewSlotNg; + #dom: XFoldviewNg; static observedAttributes = []; - constructor(dom: XFoldviewSlotNg) { + constructor(dom: XFoldviewNg) { this.#dom = dom; this.#dom.addEventListener('touchmove', this.#handleTouch, { @@ -82,15 +80,15 @@ export class XFoldviewSlotNgTouchEventsHandler } #handleTouch = (event: TouchEvent) => { - const parentElement = this.#getParentElement(); - if (!parentElement) { + if (this.#dom.getAttribute('scroll-enable') === 'false') { return; } + const touch = event.touches.item(0)!; - const { pageY, pageX } = touch; - const deltaY = this.#previousPageY! - pageY; + const { clientX, clientY } = touch; + const deltaY = this.#previousPageY! - clientY; if (this.#scrollingVertically === null) { - const deltaX = this.#previousPageX! - pageX; + const deltaX = this.#previousPageX! - clientX; this.#scrollingVertically = Math.abs(deltaY) > Math.abs(deltaX); } if (this.#scrollingVertically === false) { @@ -99,13 +97,12 @@ export class XFoldviewSlotNgTouchEventsHandler if (event.cancelable) { event.preventDefault(); } - this.#handleScrollDelta(deltaY, parentElement); - this.#previousPageY = pageY; + this.#handleScrollDelta(deltaY); + this.#previousPageY = clientY; }; #handleWheel = (event: WheelEvent) => { - const parentElement = this.#getParentElement(); - if (!parentElement) { + if (this.#dom.getAttribute('scroll-enable') === 'false') { return; } if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) { @@ -122,7 +119,6 @@ export class XFoldviewSlotNgTouchEventsHandler 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); @@ -131,24 +127,28 @@ export class XFoldviewSlotNgTouchEventsHandler if (event.cancelable) { event.preventDefault(); } - this.#handleScrollDelta(event.deltaY, parentElement); + this.#handleScrollDelta(event.deltaY); }; - #getParentElement(): XFoldviewNg | void { - const parentElement = this.#dom.parentElement; - if (parentElement && parentElement.tagName === 'X-FOLDVIEW-NG') { - return parentElement as XFoldviewNg; - } - } + // Removed #getParentElement #touchStart = (event: TouchEvent) => { const { pageX, pageY } = event.touches.item(0)!; + // For nested foldviews, we only handle if this foldview is the closest one + const pathElements = event.composedPath(); + const closestFoldview = pathElements.find(el => + el instanceof Element && el.tagName === 'X-FOLDVIEW-NG' + ); + if (closestFoldview !== this.#dom) { + this.#elements = []; + return; + } + this.#elements = document.elementsFromPoint(pageX, pageY).filter(e => - this.#dom.contains(e) && e !== this.#dom + this.#dom.contains(e) && this.#dom !== e ); this.#previousPageY = pageY; this.#previousPageX = pageX; - this.#parentScrollTop = this.#getParentElement()?.scrollTop ?? 0; for (const element of this.#elements) { this.#childrenElemsntsScrollTop.set(element, element.scrollTop); } @@ -159,10 +159,9 @@ export class XFoldviewSlotNgTouchEventsHandler #touchEnd = () => { this.#scrollingVertically = null; if (this.#currentScrollingElement) { - const parentElement = this.#getParentElement(); if ( - this.#currentScrollingElement === parentElement - && !parentElement[isHeaderShowing] + this.#currentScrollingElement === this.#dom + && !this.#dom[isHeaderShowing] ) { return; } @@ -175,24 +174,18 @@ export class XFoldviewSlotNgTouchEventsHandler #handleScrollDelta( deltaY: number, - parentElement: XFoldviewNg, ) { const scrollableKidY = this.#getTheMostScrollableKid(deltaY); if ( - (parentElement[isHeaderShowing] && deltaY > 0 + (this.#dom[isHeaderShowing] && deltaY > 0 || (deltaY < 0 && !scrollableKidY)) // deltaY > 0: swipe up (folding header) // scroll the foldview if its scrollable - || (!parentElement[isHeaderShowing] && !scrollableKidY) + || (!this.#dom[isHeaderShowing] && !scrollableKidY) // all sub doms are scrolled ) { - parentElement.scrollBy({ - top: deltaY, - behavior: 'smooth', - }); - this.#parentScrollTop += deltaY; - parentElement.scrollTop = this.#parentScrollTop; - this.#currentScrollingElement = parentElement; + this.#dom.scrollTop += deltaY; + this.#currentScrollingElement = this.#dom; } else if (scrollableKidY) { this.#currentScrollingElement = scrollableKidY; this.#scrollKid(scrollableKidY, deltaY); diff --git a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNg.ts b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNg.ts index 24b21c3045..c4305e00ee 100644 --- a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNg.ts +++ b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewSlotNg.ts @@ -5,8 +5,6 @@ */ import { Component } from '../../element-reactive/index.js'; import { CommonEventsAndMethods } from '../common/CommonEventsAndMethods.js'; -import { XFoldviewSlotNgTouchEventsHandler } from './XFoldviewSlotNgTouchEventsHandler.js'; -import { slotKid, type XFoldviewNg } from './XFoldviewNg.js'; import { LinearContainer } from '../../compat/index.js'; @Component( @@ -14,13 +12,7 @@ import { LinearContainer } from '../../compat/index.js'; [ LinearContainer, CommonEventsAndMethods, - XFoldviewSlotNgTouchEventsHandler, ], ) export class XFoldviewSlotNg extends HTMLElement { - connectedCallback() { - if (this.matches('x-foldview-ng>x-foldview-slot-ng:first-of-type')) { - (this.parentElement as XFoldviewNg | null)![slotKid] = this; - } - } } diff --git a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewToolbarNg.ts b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewToolbarNg.ts index 9554d7f1e2..3631120724 100644 --- a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewToolbarNg.ts +++ b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewToolbarNg.ts @@ -5,28 +5,42 @@ */ import { Component } from '../../element-reactive/index.js'; import { CommonEventsAndMethods } from '../common/CommonEventsAndMethods.js'; -import { resizeObserver, type XFoldviewNg } from './XFoldviewNg.js'; -import { getCombinedDirectParentElement } from '../common/getCombinedParentElement.js'; import { LinearContainer } from '../../compat/index.js'; +import { updateToolbarHeight, XFoldviewNg } from './XFoldviewNg.js'; @Component('x-foldview-toolbar-ng', [ LinearContainer, CommonEventsAndMethods, ]) export class XFoldviewToolbarNg extends HTMLElement { - #parentResizeObserver: ResizeObserver | undefined = undefined; + #resizeObserver?: ResizeObserver; + connectedCallback() { - const parentElement = getCombinedDirectParentElement( - this, - 'X-FOLDVIEW-NG', - ); - this.#parentResizeObserver = parentElement?.[resizeObserver]; - this.#parentResizeObserver?.observe(this); + this.#resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + this.#updateParent(entry.contentRect.height); + } + }); + this.#resizeObserver.observe(this); + this.#updateParent(this.clientHeight); + } + + disconnectedCallback() { + this.#resizeObserver?.disconnect(); + this.#resizeObserver = undefined; } - dispose() { - this.#parentResizeObserver?.unobserve( - this, - ); + #updateParent(height: number) { + let parent = this.parentElement; + while (parent) { + if (parent instanceof XFoldviewNg) { + parent[updateToolbarHeight](height); + break; + } + if (parent.tagName !== 'LYNX-WRAPPER') { + break; + } + parent = parent.parentElement; + } } } diff --git a/packages/web-platform/web-elements/src/elements/XFoldViewNg/x-foldview-ng.css b/packages/web-platform/web-elements/src/elements/XFoldViewNg/x-foldview-ng.css index d044455304..455752d7e5 100644 --- a/packages/web-platform/web-elements/src/elements/XFoldViewNg/x-foldview-ng.css +++ b/packages/web-platform/web-elements/src/elements/XFoldViewNg/x-foldview-ng.css @@ -5,12 +5,10 @@ */ x-foldview-ng { display: flex; - overflow-y: scroll !important; - overflow-x: clip; - overflow-x: hidden; - overscroll-behavior: contain; - --foldview-header-height: 0px; - scrollbar-width: none; + overflow: hidden; + overflow: clip !important; + --foldview-scroll-top: 0px; + --foldview-scroll-height: 0px; } x-foldview-ng::-webkit-scrollbar { @@ -29,10 +27,6 @@ x-foldview-ng:not([scroll-bar-enable], [scroll-bar-enable="true"])::-webkit-scro display: none; } -x-foldview-ng[scroll-enable="false"] { - overflow-y: hidden; -} - x-foldview-ng > *, x-foldview-header-ng, x-foldview-slot-ng, @@ -48,6 +42,7 @@ x-foldview-ng > lynx-wrapper > x-foldview-slot-ng, x-foldview-ng > lynx-wrapper > x-foldview-toolbar-ng { display: flex; } + x-foldview-toolbar-ng { order: 1; position: sticky; @@ -59,6 +54,7 @@ x-foldview-header-ng { order: 2; flex: 0 0 auto; position: absolute; + transform: translateY(var(--foldview-scroll-top)); } x-foldview-ng[header-over-slot] > x-foldview-slot-ng, @@ -69,7 +65,11 @@ x-foldview-ng[header-over-slot] > lynx-wrapper > x-foldview-slot-ng { x-foldview-slot-ng { contain: strict; order: 3; + transform: translateY( + calc(var(--foldview-scroll-height) + var(--foldview-scroll-top)) + ); } + x-foldview-slot-ng scroll-view { /* avoiding default bounce on ios safari */ overscroll-behavior-y: none; diff --git a/packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/item-fixed.html b/packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/item-fixed.html index 70ff637084..2869b8d230 100644 --- a/packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/item-fixed.html +++ b/packages/web-platform/web-elements/tests/fixtures/x-foldview-ng/item-fixed.html @@ -45,7 +45,7 @@ style="height: 100vh; width: 100vw; display: flex; --lynx-display: flex; --lynx-display-toggle: var(--lynx-display-flex); flex-direction: row" > { }, { title }) => { await gotoWebComponentPage(page, title); await wait(500); - expect(page.locator('x-foldview-slot-ng')).toHaveCSS('top', '200px'); + expect(page.locator('x-foldview-slot-ng')).toHaveCSS( + 'transform', + 'matrix(1, 0, 0, 1, 0, 200)', + ); }); test('x-foldview-ng/size-parent-grow-children-specific', async ({ page, browserName, }, { title }) => { - test.skip(browserName === 'webkit', 'z-index issues for safari'); + // test.skip(browserName === 'webkit', 'z-index issues for safari'); await gotoWebComponentPage(page, title); await diffScreenShot(page, title, '300px-inf'); }); @@ -1271,7 +1274,7 @@ test.describe('web-elements test suite', () => { }, ); - test('x-foldview-ng/item-fixed', async ({ page }, { title }) => { + test.fixme('x-foldview-ng/item-fixed', async ({ page }, { title }) => { await gotoWebComponentPage(page, title); await wait(100); await diffScreenShot(page, title, 'initial'); diff --git a/packages/web-platform/web-elements/tests/web-elements.spec.ts-snapshots/x-foldview-ng/basic-with-lynx-wrapper/index-chromium-linux.png b/packages/web-platform/web-elements/tests/web-elements.spec.ts-snapshots/x-foldview-ng/basic-with-lynx-wrapper/index-chromium-linux.png index f98fca980489714159bd0a8923aa7be4e4e07eff..a05f4b09b46709384d6e995523dc6f7a58098fc5 100644 GIT binary patch delta 157 zcmca3d`NhLieb5@i(^Q|oHy4T1rIrhuw0zKO2W}iq`*;~ZHIFA>!iiOzvs2ToK<{H z>H*)jTdyn`Y&R#eH8b&-tov@bVe{ delta 197 zcmX>kd`EbKisM927srr_Id85V^khzyaJ`tH5cW;sU{*^4>&sJ0InL4r&(#tPC(CWx zb-3P-HNUj-_eawWQ{I-Hv7B4_J@^~ri;J5bSqvB_C$R8xEvnsjpM6@9^shfKF+ss? zzt7husIH&>-*Wl%?fsE$_cv~IVC1=2_jfPfnkVl+Sw8u>@uDLmd+zZ<9*3U$n+4fA vnI}G&squw@f#LuEOX){002yo`Co(e}W{{dNX@|2UP=vwL)z4*}Q$iB}FXC54 diff --git a/packages/web-platform/web-elements/tests/web-elements.spec.ts-snapshots/x-foldview-ng/basic-with-lynx-wrapper/index-firefox-linux.png b/packages/web-platform/web-elements/tests/web-elements.spec.ts-snapshots/x-foldview-ng/basic-with-lynx-wrapper/index-firefox-linux.png index c66d56003ff16b8ba22225ad896316008f492d48..0f1916a5ce355aa89fb861f398815624abc39be9 100644 GIT binary patch literal 20050 zcmeHPU1$_n6h1Q<<7|=*6R9GJ)Jd$aVjxmtx3vYwvPrW@f{2)ank>?XQrpCa(1fOK z?$m;9B}((q6w;rCycJARK`>(2ZB-<+5+x66%<8tKS;3a36sjpnws$5wdv`K|Ptrbo zPc!V^d*|FU=R4;+=N{(cJq@q;3rY)!$RDl??Il{lZ{=%hKEF)7m)K9FQ#iD1Usv<* zQ)8#!JW%rC_3lGw^rDJ*dEvol^`|TJ;Pk~)v29aZu72~@!s*d>I_`)6n4DXAB(N;h)s`7W1wgEiBLZlaE@EB)kgTGFdy=+R(`e;qZ-nIavK*bv z6_gLmljUT*ueXj%lDABfMuVmkW8=Pug+|c50VQ8i;^xM(PvgfK-K%!*KF%0DKl18^ zRhmbVo2xbI>}+e77iG@|B));jcueeDT+suw^u0fL}K6z z7V~ZZs)Q;XOu{O?_yE8?9tyq^VJ92w5OzHHKm-RFa_|9P318{hf(~B^Uzy?B5%5Zn zEx=d8SHf2^REAJwvkR9`vR*mAICrHg?LWFQr=IyzV0L)t!e66pe}AX+$vqE$Y3ZBj z9w0NyXzis3)lQX&*Bd(}X|^@hKfvpvGk5CilQCX2eKDtKnhf&+<7emYWu}@Ve2He3 z@`-}%IiJdc!Pt~j?UbWsSO7ZUh6Fo+4%%S{`~(Pg03BE-fv^MU;D2NX`^G|_JG}te z1AS0F-GC1M8y&=Y7XL}DFUm<}w66MOOHcatk*|O2_Jw%+!{*qP>@_C|DNtGC3$X({ z326+rwaT&geQYRe(u~XipBlI^GkA=5qiA<0mGAkrOZqC1EmIiDO*-9~o`2n`=c%j} z3qS`vyM-M<2jj2<-W&%zfDWuCN!S5&06SP}7zP%A1q%!OAZB8xD|K;>K{|MH@@4n- z)j8)bnGDBFb5~kyE^A@u^Nep9HC;G3SmLZ9QXOxjxi!W-EJ-6>qqQ4ELgQpFI-U1E zS&}dF4Cv4H-dbmvnCR_}x5s?KQ>dSRE$#J6%iH4f-EHyqn5PO=cKL_xrH^>Jr={N> zvw4k1C;E?6yZhm7!Q7QIN9H}1I<2X?@mg-en9-QGdESyR+GJ2=^U(E2NbAP2BQsva zG7H3u5XJ^XI51iA32;pUEO1S3ABz|v;wFN%ARBflN3a%P0cKhReJ7SLCb2ic(( zd4vE95;_rKEnbR*PJrd{nf2f_l@b+8)^`EitX>C|717yJDX6Rf8mO#@&Y-eRT`r>& zP+1Y3t*Q~ItN;tBtccE_vZ8KgTL`GEh_IltZaVsSD(fB(`YH}AY#s~xD!>K$Dk8E? b-WL5em#F6A%#U07x8f*V(-0bZIr723)au<) literal 21017 zcmeI4eN0FmBm&|2}-7Gp6mtZQH*|M%c8&PZ2Ze7fr+vo@CDw(d9Ok)v4J%!%B*Tz5Q{B!58 z2f63o+k4(S@0{N`_r5pkY97i=Uy@D;$*g|3vYwC>_>*<3QsHv;WzQBu1X5jDzRA#h z?fZM$hd$4JwawvVrU!e+#Ff6Cy1yLf#RmQU|e-~{$xyqTD5$;bbb{N=lV=Ys1Ot(5N#IV~KCbZ$_ zvBwyuqkGDt_3IeMeQ&gXOlF{7Bp+=xRV+y19Kz^1?o*a+H85YOWswdwPjv00mvVWc z*Z&saI8hhx4L{=%h8gO7E-P;lPj-i^)T#`XG?B8nRu*w+MKWxi-q9eEanFbeMjKK5 zKJIRq_^T78-E|^a_`_1Iuu>qe-O;*MA)rKrd_$2m-p( z#5e#0A&dku6uv0s?947Jbego17%Ms`iWN;z zthhYgxe1rsC_BoibPBoic)no8l1 z=~~QJ-&zTKbJ^^Svv{Fg5m52l)#2-A`^mhEFQipM#bUXAf_oph_ibN?qHth=ioxbH z`a!r{4R}lrC)7@Ikx-CO;<$^5hKPoUhKPoUcC(0vScY7RV2+uITQ4(VZh0lrD#z0C zm{Rm>;TVQ%9fCHf_@omvR3F)( z_3Yvc=js0nB`57>qxT7K2i^|ZOwc+KOwEYaf!2Z6p+eu07#E-`myIGGm7(uI-!W$< zXdVB{I(*G*tc2uz8T~E*GDyOg1*l#T2@we?JBtz$dLr(lb)a>ib+Acar9t0;>BK~e zi)K^NccAZ>PjR7jpmoUQ5-df@5k&O4K%CRa?t@Q0lIry} Km6iuvd;bA_5J1NO diff --git a/packages/web-platform/web-elements/tests/web-elements.spec.ts-snapshots/x-foldview-ng/basic-with-lynx-wrapper/index-webkit-linux.png b/packages/web-platform/web-elements/tests/web-elements.spec.ts-snapshots/x-foldview-ng/basic-with-lynx-wrapper/index-webkit-linux.png index 313001a23d9e839e10d8ddf39c7705a7be69003d..620237e4d39dcc43ebb428a273108583500374c5 100644 GIT binary patch delta 196 zcmX>jv_xovqy0Hg7srr_Id89T%yn`UaXt9AgX>PC>xa9{tQLii>rSLz5NC`!uI}o@ zow2M*dhg=xy_U}zDxCXn*Yd4qXZR4axsX|qaqPk}0;g*qPZs9ArH!RFHS%%0k>9C4j+DXxy(bC%)#3=b-Q qYz|~L1RJ_Jfmw-hasrdA*bnyB)xy&&s*c`f00K`}KbLh*2~7ZYiC1p` literal 2380 zcmeAS@N?(olHy`uVBq!ia0y~yU~FSxV4A_f1{5jZ5VJUX< z4B-HR8jh3>1_q9Ao-U3d6?5KRU+CK$DB<=nIw;h86-N`N)&&ozOA3jMY)RZAXBfB* z7^F8a{BxK0zI11r&3Tsfzx?)H{bV$a@lSOAbpFR5pOt>=(UjS~-*UDsL&IYw9)=bJ z27yBi4jjx3ilZD7!{GJDI`eAg0*~vv4fE3aA58fn!N8HopwPnDAi&1pG|C}83?gc4 zYjiieGqi8M`KRae+wX5AgLBrsOZT_%v>Odk3RBg!-{=3n;XS~3H}1``&)+h?@%VN0 zkET@0a%;!cO**IF2r|g<%a0bIqm{^LMM~|)#QrXJ$$d3XBAkIOA_h-aKbLh*2~7YQ CNT^K! diff --git a/packages/web-platform/web-elements/tests/web-elements.spec.ts-snapshots/x-foldview-ng/size-parent-grow-children-specific/300px-inf-webkit-linux.png b/packages/web-platform/web-elements/tests/web-elements.spec.ts-snapshots/x-foldview-ng/size-parent-grow-children-specific/300px-inf-webkit-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..dcc7dda37c438d46dbb05cce6cf1ad5d22f1cc7f GIT binary patch literal 2432 zcmeAS@N?(olHy`uVBq!ia0y~yU~FSxV4A_f1{5jZ5VJUX< z4B-HR8jh3>1_n+oPZ!6KiaBquZ_Ek_lyJLfZPeTRa#3za!t`>FOyT9H? zD<1l?l2tnUpxcH(1G(3CB|R*R&qRHFI&b~$KdVn~e}9b0X2th>z4ve9ldFrDzt1<{ zKkfc;`*&~u)^4)gJHs|n-*%QQL&IGq9)=bJ27yBi4jjx3ilZD7!{GHz<_7{jZ_mEr z{BS>JH1tReJ@iz!=2&~`89B!GtUomy|NQ)x^{t2f^Ym9)-u^xN^5~ZB-)D9+ zHVCjWI7u*YBr+(pjB-d0gO0DCUOs)pTd+e;Uib9JH-Z6rqrpjGlDZZj7k~C11N-Zf z-`Ahsd|p2Nd_#fZXrW43#j>$(-`;t985?9DC5>iu$_l$A(_<2w-5EAm534Hb5A)7j X8cU6jZPf&}q8L0~{an^LB{Ts5=HSwA literal 0 HcmV?d00001 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 index 37a221ce52..aac20e23d7 100644 --- 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 @@ -23,33 +23,33 @@ test.describe('x-foldview-ng wheel', () => { }) => { test.skip(browserName === 'webkit', 'mouse wheel unsupported on webkit'); await goto(page, title); - const foldview = page.locator('#foldview'); + await wait(200); 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 page.locator('#inner-scroll').hover({ + force: true, + position: { x: 50, y: 50 }, }); - const foldviewInitial = await foldview.evaluate((dom: HTMLElement) => - dom.scrollTop + const foldviewInitial = await page.evaluate(() => + (document.querySelector('#foldview') as HTMLElement).scrollTop ); - const scrollViewInitial = await scrollview.evaluate((dom: HTMLElement) => - dom.scrollTop + const scrollViewInitial = await page.evaluate(() => + (document.querySelector('#inner-scroll') as HTMLElement).scrollTop ); await page.mouse.wheel(0, 120); await wait(200); expect( - await foldview.evaluate((dom: HTMLElement) => dom.scrollTop), + await page.evaluate(() => + (document.querySelector('#foldview') as HTMLElement).scrollTop + ), 'wheel-outer-scrolls-first', ).toBeGreaterThan(foldviewInitial); expect( - await scrollview.evaluate((dom: HTMLElement) => dom.scrollTop), + await page.evaluate(() => + (document.querySelector('#inner-scroll') as HTMLElement).scrollTop + ), 'wheel-inner-not-scrolled-before-header', ).toBe(scrollViewInitial); }); @@ -59,34 +59,38 @@ test.describe('x-foldview-ng wheel', () => { }) => { test.skip(browserName === 'webkit', 'mouse wheel unsupported on webkit'); await goto(page, title); - const foldview = page.locator('#foldview'); + await wait(200); 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 page.locator('#inner-scroll').hover({ + force: true, + position: { x: 50, y: 50 }, }); - await foldview.evaluate((dom: HTMLElement) => { - dom.scrollTop = dom.scrollHeight; + await page.evaluate(() => { + const foldview = document.querySelector('#foldview') as HTMLElement; + const scrollview = document.querySelector('#inner-scroll') as HTMLElement; + foldview.scrollTop = 0; + scrollview.scrollTop = 0; + foldview.scrollTop = foldview.scrollHeight; }); await wait(100); - const foldviewBeforeInner = await foldview.evaluate((dom: HTMLElement) => - dom.scrollTop + const foldviewBeforeInner = await page.evaluate(() => + (document.querySelector('#foldview') as HTMLElement).scrollTop ); await page.mouse.wheel(0, 200); await wait(200); expect( - await scrollview.evaluate((dom: HTMLElement) => dom.scrollTop), + await page.evaluate(() => + (document.querySelector('#inner-scroll') as HTMLElement).scrollTop + ), 'wheel-continues-to-inner-scroll', ).toBeGreaterThan(0); expect( - await foldview.evaluate((dom: HTMLElement) => dom.scrollTop), + await page.evaluate(() => + (document.querySelector('#foldview') as HTMLElement).scrollTop + ), 'wheel-outer-stays-at-end', ).toBeGreaterThanOrEqual(foldviewBeforeInner); }); From 61799788fe056bdc3d87e445b2e79fc102dd5637 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:12:41 +0800 Subject: [PATCH 2/3] + changeset --- .changeset/slimy-cows-skip.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/slimy-cows-skip.md diff --git a/.changeset/slimy-cows-skip.md b/.changeset/slimy-cows-skip.md new file mode 100644 index 0000000000..f55dcba070 --- /dev/null +++ b/.changeset/slimy-cows-skip.md @@ -0,0 +1,9 @@ +--- +"@lynx-js/web-elements": minor +--- + +feat: reimplement `XFoldViewNg` scrolling using CSS transforms and custom scroll handling, updating related events, styles, and tests. + +this breaks https://github.com/lynx-family/lynx-stack/pull/878 + +The position:fixed elements in x-foldview-header-ng and x-foldview-slot-ng will be affected. From bf134ef2c3ce0ef2b50d2358e92d3f809d8ec56f Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:33:06 +0800 Subject: [PATCH 3/3] refactor: improve scrollTop value clamping and switch touch event handling to use clientX/Y coordinates. --- .../src/elements/XFoldViewNg/XFoldviewNg.ts | 7 ++----- .../XFoldviewNgTouchEventsHandler.ts | 18 +++++++++--------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNg.ts b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNg.ts index 1fab566c97..255f38360f 100644 --- a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNg.ts +++ b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNg.ts @@ -62,11 +62,8 @@ export class XFoldviewNg extends HTMLElement { } override set scrollTop(value: number) { - if (value > this.scrollHeight) { - value = this.scrollHeight; - } else if (value < 0) { - value = 0; - } + const maxScroll = Math.max(this.scrollHeight, 0); + value = Math.max(0, Math.min(value, maxScroll)); if (this.#scrollTop === value) { return; } diff --git a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNgTouchEventsHandler.ts b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNgTouchEventsHandler.ts index 76f82af24d..95b5a5c5e0 100644 --- a/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNgTouchEventsHandler.ts +++ b/packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNgTouchEventsHandler.ts @@ -10,8 +10,8 @@ export class XFoldviewNgTouchEventsHandler { #childrenElemsntsScrollTop: WeakMap = new WeakMap(); #elements?: Element[]; - #previousPageY: number = 0; - #previousPageX: number = 0; + #previousClientY: number = 0; + #previousClientX: number = 0; #scrollingVertically: boolean | null = null; #currentScrollingElement?: Element; #deltaY: number = 0; @@ -86,9 +86,9 @@ export class XFoldviewNgTouchEventsHandler const touch = event.touches.item(0)!; const { clientX, clientY } = touch; - const deltaY = this.#previousPageY! - clientY; + const deltaY = this.#previousClientY! - clientY; if (this.#scrollingVertically === null) { - const deltaX = this.#previousPageX! - clientX; + const deltaX = this.#previousClientX! - clientX; this.#scrollingVertically = Math.abs(deltaY) > Math.abs(deltaX); } if (this.#scrollingVertically === false) { @@ -98,7 +98,7 @@ export class XFoldviewNgTouchEventsHandler event.preventDefault(); } this.#handleScrollDelta(deltaY); - this.#previousPageY = clientY; + this.#previousClientY = clientY; }; #handleWheel = (event: WheelEvent) => { @@ -133,7 +133,7 @@ export class XFoldviewNgTouchEventsHandler // Removed #getParentElement #touchStart = (event: TouchEvent) => { - const { pageX, pageY } = event.touches.item(0)!; + const { clientX, clientY } = event.touches.item(0)!; // For nested foldviews, we only handle if this foldview is the closest one const pathElements = event.composedPath(); const closestFoldview = pathElements.find(el => @@ -144,11 +144,11 @@ export class XFoldviewNgTouchEventsHandler return; } - this.#elements = document.elementsFromPoint(pageX, pageY).filter(e => + this.#elements = document.elementsFromPoint(clientX, clientY).filter(e => this.#dom.contains(e) && this.#dom !== e ); - this.#previousPageY = pageY; - this.#previousPageX = pageX; + this.#previousClientY = clientY; + this.#previousClientX = clientX; for (const element of this.#elements) { this.#childrenElemsntsScrollTop.set(element, element.scrollTop); }