-
Notifications
You must be signed in to change notification settings - Fork 122
feat: reimplement XFoldViewNg scrolling using CSS transforms and cust… #2299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,57 +6,108 @@ | |
| 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<typeof XFoldviewNg>('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(); | ||
|
|
||
| [updateHeaderHeight](height: number) { | ||
| this.#headerHeight = height; | ||
| this.style.setProperty( | ||
| '--foldview-scroll-height', | ||
| this.scrollHeight + 'px', | ||
| ); | ||
| } | ||
|
|
||
| get [scrollableLength](): number { | ||
| [updateToolbarHeight](height: number) { | ||
| this.#toolbarHeight = height; | ||
| this.style.setProperty( | ||
| '--foldview-scroll-height', | ||
| this.scrollHeight + 'px', | ||
| ); | ||
| } | ||
|
|
||
| override get scrollHeight(): number { | ||
| return this.#headerHeight - this.#toolbarHeight; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| 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]; | ||
| } 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; | ||
| } | ||
| 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; | ||
| } | ||
| } | ||
|
Comment on lines
+83
to
+97
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's find and examine the XFoldViewNg file
fd "XFoldviewNg.ts" packages/web-platform/web-elements/src/Repository: lynx-family/lynx-stack Length of output: 140 🏁 Script executed: # Get the file and see the full context around lines 73 and 83-97
cat -n packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNg.ts | head -120 | tail -60Repository: lynx-family/lynx-stack Length of output: 2224 🏁 Script executed: # Also check the entire file structure to understand the class
wc -l packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNg.tsRepository: lynx-family/lynx-stack Length of output: 144 🏁 Script executed: # Check the scrollBy method to see if there's a pattern
cat -n packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNg.ts | sed -n '99,111p'Repository: lynx-family/lynx-stack Length of output: 579 🏁 Script executed: # Look for similar scrollTo implementations in other element files
fd "\.ts$" packages/web-platform/web-elements/src/elements/ | xargs grep -l "scrollTo" | head -5Repository: lynx-family/lynx-stack Length of output: 441 🏁 Script executed: # Test what happens with NaN/Infinity in the clamping logic using node
node <<'EOF'
// Test Math.max/Math.min with NaN and Infinity
const maxScroll = 100;
// NaN case
let valueNaN = NaN;
valueNaN = Math.max(0, Math.min(valueNaN, maxScroll));
console.log('NaN result:', valueNaN, 'toString:', valueNaN.toString() + 'px');
// Infinity case
let valueInf = Infinity;
valueInf = Math.max(0, Math.min(valueInf, maxScroll));
console.log('Infinity result:', valueInf, 'toString:', valueInf.toString() + 'px');
// Test null destructuring
try {
const { top } = null;
console.log('null destructure: no error');
} catch(e) {
console.log('null destructure error:', e.message);
}
EOFRepository: lynx-family/lynx-stack Length of output: 217 🏁 Script executed: # Let's verify the exact clamping behavior with NaN more carefully
node <<'EOF'
const maxScroll = 100;
// Detailed NaN test
let value = NaN;
console.log('Initial:', value);
console.log('After Math.min(value, maxScroll):', Math.min(value, maxScroll));
console.log('After Math.max(0, Math.min(value, maxScroll)):', Math.max(0, Math.min(value, maxScroll)));
// Verify Number.isFinite
console.log('Number.isFinite(NaN):', Number.isFinite(NaN));
console.log('Number.isFinite(Infinity):', Number.isFinite(Infinity));
console.log('Number.isFinite(100):', Number.isFinite(100));
EOFRepository: lynx-family/lynx-stack Length of output: 255 🏁 Script executed: # Check if scrollBy has the same issue and if it's also affected
cat -n packages/web-platform/web-elements/src/elements/XFoldViewNg/XFoldviewNg.ts | sed -n '101,111p'Repository: lynx-family/lynx-stack Length of output: 462 Add guards against Line 84 accepts 🐛 Proposed fix override scrollTo(arg1?: any, arg2?: any): void {
- if (typeof arg1 === 'object') {
+ if (typeof arg1 === 'object' && arg1 !== null) {
const { top, behavior } = arg1;
- if (typeof top === 'number') {
+ if (typeof top === 'number' && Number.isFinite(top)) {
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') {
+ } else if (typeof arg2 === 'number' && Number.isFinite(arg2)) {
this.scrollTop = arg2;
}
}
override scrollBy(arg1?: any, arg2?: any): void {
- if (typeof arg1 === 'object') {
+ if (typeof arg1 === 'object' && arg1 !== null) {
const { top, behavior } = arg1;
this.scrollTo({
top: (typeof top === 'number' ? top : 0) + this.scrollTop,
behavior,
});
} else {
this.scrollTo(0, this.scrollTop + (arg2 || 0));
}
}🤖 Prompt for AI Agents |
||
|
|
||
| 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)); | ||
| } | ||
| super.scrollTop = value; | ||
| } | ||
|
|
||
| setFoldExpanded(params: { offset: string; smooth: boolean }) { | ||
|
|
@@ -71,9 +122,4 @@ export class XFoldviewNg extends HTMLElement { | |
| get [scrollContainerDom]() { | ||
| return this; | ||
| } | ||
|
|
||
| disconnectedCallback() { | ||
| this[resizeObserver]?.disconnect(); | ||
| this[resizeObserver] = undefined; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: lynx-family/lynx-stack
Length of output: 1496
🏁 Script executed:
Repository: lynx-family/lynx-stack
Length of output: 9923
🏁 Script executed:
Repository: lynx-family/lynx-stack
Length of output: 1461
Consider documenting the rationale for
'last 1 Chrome versions'in a comment.The narrower browser target is intentional—
web-elementshas a documentedcompatmodule (e.g.,LinearContainer) with polyfills for missing features. Settingpolyfill: 'off'is consistent across web-platform packages and allows the package to manage its own compatibility layer. However, adding a comment inrsbuild.config.tsexplaining why this target was chosen would improve clarity for future maintenance, especially since it's narrower than other web-platform packages (e.g.,'last 2 Chrome versions'inweb-rsbuild-server-middleware).🤖 Prompt for AI Agents