fix(stage): 修复切换页面后 StageMask 尺寸与滚动偏移异常

pageResize 仅同步当前页且已挂载节点,忽略 0 尺寸回调;observe 时立即同步并在 destroy 时断开 intersectionObserver。
This commit is contained in:
roymondchen 2026-08-04 18:58:52 +08:00
parent d5a2e25e18
commit a9cda719bc
2 changed files with 149 additions and 6 deletions

View File

@ -117,19 +117,27 @@ export default class StageMask extends Rule {
this.page = page;
this.initObserverIntersection();
this.initObserverWrapper();
// 页面切换后立即同步一次ResizeObserver 首次回调可能仍上报旧页/0 尺寸
this.syncPageSize();
}
/**
* mask大小
* mask
* @description page entry 0
* @param entries ResizeObserverEntry
*/
public pageResize(entries: ResizeObserverEntry[]): void {
const [entry] = entries;
const { clientHeight, clientWidth } = entry.target;
this.setHeight(clientHeight);
this.setWidth(clientWidth);
if (!this.page || !entries.length) return;
this.scroll();
// 取当前 page 对应 entry避免 batch 中首项是旧页时整批被丢弃
const entry = entries.find((item) => item.target === this.page);
if (!entry) return;
const { target } = entry;
// 旧页卸载后仍可能回调,已断开连接的节点不再同步尺寸
if (!target.isConnected) return;
this.applyPageSize(target.clientWidth, target.clientHeight);
}
/**
@ -186,6 +194,7 @@ export default class StageMask extends Rule {
this.content?.remove();
this.page = null;
this.pageScrollParent = null;
this.intersectionObserver?.disconnect();
this.wrapperResizeObserver?.disconnect();
}
@ -287,6 +296,26 @@ export default class StageMask extends Rule {
this.content.dispatchEvent(event);
}
/**
*
*/
private syncPageSize(): void {
if (!this.page?.isConnected) return;
this.applyPageSize(this.page.clientWidth, this.page.clientHeight);
}
/**
* 0 / editor-mask
*/
private applyPageSize(clientWidth: number, clientHeight: number): void {
if (clientWidth <= 0 || clientHeight <= 0) return;
this.setHeight(clientHeight);
this.setWidth(clientWidth);
this.scroll();
}
/**
*
* @param height

View File

@ -79,11 +79,124 @@ describe('StageMask', () => {
const page = globalThis.document.createElement('div');
Object.defineProperty(page, 'clientWidth', { value: 400, configurable: true });
Object.defineProperty(page, 'clientHeight', { value: 300, configurable: true });
globalThis.document.body.appendChild(page);
mask.observe(page);
mask.pageResize([makeResizeEntry(page)]);
expect(mask.width).toBe(400);
expect(mask.height).toBe(300);
expect(mask.content.style.width).toBe('400px');
expect(mask.content.style.height).toBe('300px');
});
test('observe 立即同步尺寸,并从长页切到短页时修正滚动偏移', () => {
mask = new StageMask({ disabledRule: true });
const tallPage = globalThis.document.createElement('div');
Object.defineProperty(tallPage, 'clientWidth', { value: 400, configurable: true });
Object.defineProperty(tallPage, 'clientHeight', { value: 1000, configurable: true });
globalThis.document.body.appendChild(tallPage);
mask.observe(tallPage);
mask.wrapperWidth = 400;
mask.wrapperHeight = 300;
(mask as any).setMaxScrollLeft();
(mask as any).setMaxScrollTop();
mask.scrollTop = 500;
(mask as any).scroll();
expect(mask.scrollTop).toBe(500);
const shortPage = globalThis.document.createElement('div');
Object.defineProperty(shortPage, 'clientWidth', { value: 400, configurable: true });
Object.defineProperty(shortPage, 'clientHeight', { value: 400, configurable: true });
globalThis.document.body.appendChild(shortPage);
mask.observe(shortPage);
expect(mask.width).toBe(400);
expect(mask.height).toBe(400);
expect(mask.maxScrollTop).toBe(100);
expect(mask.scrollTop).toBe(100);
expect(mask.content.style.transform).toBe('translate3d(0px, -100px, 0)');
});
test('pageResize 忽略非当前页、已卸载页与 0 尺寸,避免切换页面后 editor-mask 变为 0', () => {
mask = new StageMask({ disabledRule: true });
const oldPage = globalThis.document.createElement('div');
Object.defineProperty(oldPage, 'clientWidth', { value: 400, configurable: true });
Object.defineProperty(oldPage, 'clientHeight', { value: 300, configurable: true });
globalThis.document.body.appendChild(oldPage);
mask.observe(oldPage);
expect(mask.width).toBe(400);
expect(mask.height).toBe(300);
const newPage = globalThis.document.createElement('div');
Object.defineProperty(newPage, 'clientWidth', { value: 500, configurable: true });
Object.defineProperty(newPage, 'clientHeight', { value: 600, configurable: true });
globalThis.document.body.appendChild(newPage);
mask.observe(newPage);
expect(mask.width).toBe(500);
expect(mask.height).toBe(600);
expect(mask.content.style.width).toBe('500px');
expect(mask.content.style.height).toBe('600px');
// 仍连接但不是当前 page忽略
Object.defineProperty(oldPage, 'clientWidth', { value: 100, configurable: true });
Object.defineProperty(oldPage, 'clientHeight', { value: 100, configurable: true });
mask.pageResize([makeResizeEntry(oldPage)]);
expect(mask.width).toBe(500);
expect(mask.height).toBe(600);
// batch 首项是旧页、后续是当前页:仍应应用当前页尺寸
Object.defineProperty(newPage, 'clientWidth', { value: 520, configurable: true });
Object.defineProperty(newPage, 'clientHeight', { value: 620, configurable: true });
mask.pageResize([makeResizeEntry(oldPage), makeResizeEntry(newPage)]);
expect(mask.width).toBe(520);
expect(mask.height).toBe(620);
// 旧页面卸载后 ResizeObserver 可能仍回调,不应覆盖当前蒙层尺寸
Object.defineProperty(oldPage, 'clientWidth', { value: 0, configurable: true });
Object.defineProperty(oldPage, 'clientHeight', { value: 0, configurable: true });
oldPage.remove();
mask.pageResize([makeResizeEntry(oldPage)]);
expect(mask.width).toBe(520);
expect(mask.height).toBe(620);
// 当前 page 已断开连接:忽略
newPage.remove();
Object.defineProperty(newPage, 'clientWidth', { value: 100, configurable: true });
Object.defineProperty(newPage, 'clientHeight', { value: 100, configurable: true });
mask.pageResize([makeResizeEntry(newPage)]);
expect(mask.width).toBe(520);
expect(mask.height).toBe(620);
expect(mask.content.style.width).toBe('520px');
expect(mask.content.style.height).toBe('620px');
// 重新挂载后短暂为 0 时也应忽略
globalThis.document.body.appendChild(newPage);
Object.defineProperty(newPage, 'clientWidth', { value: 0, configurable: true });
Object.defineProperty(newPage, 'clientHeight', { value: 0, configurable: true });
mask.pageResize([makeResizeEntry(newPage)]);
expect(mask.width).toBe(520);
expect(mask.height).toBe(620);
expect(mask.content.style.width).toBe('520px');
expect(mask.content.style.height).toBe('620px');
});
test('observe 时 page 宽高为 0 不更新蒙层尺寸', () => {
mask = new StageMask({ disabledRule: true });
const firstPage = globalThis.document.createElement('div');
Object.defineProperty(firstPage, 'clientWidth', { value: 400, configurable: true });
Object.defineProperty(firstPage, 'clientHeight', { value: 300, configurable: true });
globalThis.document.body.appendChild(firstPage);
mask.observe(firstPage);
const emptyPage = globalThis.document.createElement('div');
Object.defineProperty(emptyPage, 'clientWidth', { value: 0, configurable: true });
Object.defineProperty(emptyPage, 'clientHeight', { value: 0, configurable: true });
globalThis.document.body.appendChild(emptyPage);
mask.observe(emptyPage);
expect(mask.page).toBe(emptyPage);
expect(mask.width).toBe(400);
expect(mask.height).toBe(300);
});
test('wheel 事件更新 scroll 并 emit scroll', () => {
@ -91,6 +204,7 @@ describe('StageMask', () => {
const page = globalThis.document.createElement('div');
Object.defineProperty(page, 'clientWidth', { value: 1000, configurable: true });
Object.defineProperty(page, 'clientHeight', { value: 1000, configurable: true });
globalThis.document.body.appendChild(page);
mask.observe(page);
mask.pageResize([makeResizeEntry(page)]);
mask.wrapperWidth = 400;