修复:展开折叠块时把新展开的正文露出来
- 新增纯函数 toggleRevealDelta:折叠头冻结与露出正文合成一次 scrollTop 位移 - 正文底边超出视口只补超出量,正文比视口还高才对齐正文顶边,收起不额外滚动 - 补齐用例:纯函数六例与 freezeToggleHead 两条真机行为 Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
+87
-6
@@ -5,6 +5,7 @@ import {
|
||||
freezeToggleHead,
|
||||
resolveToggleBody,
|
||||
resolveToggleHead,
|
||||
toggleRevealDelta,
|
||||
} from './conversationToggleReveal';
|
||||
|
||||
type StubRect = { top: number; bottom: number };
|
||||
@@ -87,6 +88,77 @@ describe('resolveToggleBody', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleRevealDelta', () => {
|
||||
const listRect = { top: 0, bottom: 400 };
|
||||
|
||||
it('头部没动、正文完整放得下:不滚动', () => {
|
||||
expect(
|
||||
toggleRevealDelta({
|
||||
listRect,
|
||||
headRect: { top: 100, bottom: 120 },
|
||||
headTop: 100,
|
||||
bodyRect: { top: 120, bottom: 300, height: 180 },
|
||||
}),
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it('正文比视口矮但底边超出视口:只滚到刚好露出底边', () => {
|
||||
// 头部在视口下缘,刚展开的 200px 正文全部长在视口外:只补这 120px 的超出量。
|
||||
expect(
|
||||
toggleRevealDelta({
|
||||
listRect,
|
||||
headRect: { top: 300, bottom: 320 },
|
||||
headTop: 300,
|
||||
bodyRect: { top: 320, bottom: 520, height: 200 },
|
||||
}),
|
||||
).toBe(120);
|
||||
});
|
||||
|
||||
it('正文比视口还高且底边超出:对齐正文顶边,让用户从正文开头读起', () => {
|
||||
expect(
|
||||
toggleRevealDelta({
|
||||
listRect,
|
||||
headRect: { top: 300, bottom: 320 },
|
||||
headTop: 300,
|
||||
bodyRect: { top: 320, bottom: 900, height: 580 },
|
||||
}),
|
||||
).toBe(320);
|
||||
});
|
||||
|
||||
it('收起(正文高度归零)不额外滚动,只按头部位移补偿', () => {
|
||||
expect(
|
||||
toggleRevealDelta({
|
||||
listRect,
|
||||
headRect: { top: 10, bottom: 30 },
|
||||
headTop: -40,
|
||||
bodyRect: { top: 0, bottom: 0, height: 0 },
|
||||
}),
|
||||
).toBe(50);
|
||||
});
|
||||
|
||||
it('上方内容把头部顶下去后正文已露在视口里:不再往回滚', () => {
|
||||
expect(
|
||||
toggleRevealDelta({
|
||||
listRect,
|
||||
headRect: { top: 10, bottom: 30 },
|
||||
headTop: -40,
|
||||
bodyRect: { top: 40, bottom: 600, height: 560 },
|
||||
}),
|
||||
).toBe(50);
|
||||
});
|
||||
|
||||
it('解析不出正文:只按头部位移补偿', () => {
|
||||
expect(
|
||||
toggleRevealDelta({
|
||||
listRect,
|
||||
headRect: { top: 10, bottom: 30 },
|
||||
headTop: 10,
|
||||
bodyRect: null,
|
||||
}),
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('freezeToggleHead', () => {
|
||||
it('头部仍在视口里:按位移把头部拉回展开前的屏幕位置', () => {
|
||||
const { list, turnHead } = buildFixture();
|
||||
@@ -97,15 +169,24 @@ describe('freezeToggleHead', () => {
|
||||
expect(list.scrollTop).toBe(50);
|
||||
});
|
||||
|
||||
it('展开正文比视口还高:再把正文顶边对齐到视口顶边', () => {
|
||||
it('展开正文比视口矮但底边超出视口:滚到刚好露出正文底边', () => {
|
||||
const { list, turnHead, turnBody } = buildFixture();
|
||||
stubRect(turnHead, { top: 10, bottom: 30 });
|
||||
stubRect(turnBody, { top: 40, bottom: 600 });
|
||||
stubRect(turnHead, { top: 300, bottom: 320 });
|
||||
stubRect(turnBody, { top: 320, bottom: 520 });
|
||||
list.scrollTop = 0;
|
||||
|
||||
expect(freezeToggleHead(list, { head: turnHead, headTop: -40 })).toBe(true);
|
||||
// 头部冻结 50px 之后,正文仍比视口高且底部超出,再对齐正文顶边(+40)。
|
||||
expect(list.scrollTop).toBe(90);
|
||||
expect(freezeToggleHead(list, { head: turnHead, headTop: 300 })).toBe(true);
|
||||
expect(list.scrollTop).toBe(120);
|
||||
});
|
||||
|
||||
it('展开正文比视口还高且底边超出:对齐正文顶边', () => {
|
||||
const { list, turnHead, turnBody } = buildFixture();
|
||||
stubRect(turnHead, { top: 300, bottom: 320 });
|
||||
stubRect(turnBody, { top: 320, bottom: 900 });
|
||||
list.scrollTop = 0;
|
||||
|
||||
expect(freezeToggleHead(list, { head: turnHead, headTop: 300 })).toBe(true);
|
||||
expect(list.scrollTop).toBe(320);
|
||||
});
|
||||
|
||||
it('头部已经不在视口里:不补偿,交给锚点还原', () => {
|
||||
|
||||
+47
-20
@@ -47,6 +47,46 @@ export function resolveToggleBody(head: HTMLElement): HTMLElement | null {
|
||||
return head.ownerDocument.getElementById(controls);
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开 / 收起一个折叠块需要的总位移。纯函数:所有几何由调用方测量后传进来。
|
||||
*
|
||||
* 两段合成一次位移,不能分两次写 `scrollTop`:
|
||||
* 1. 折叠头冻结——把它拉回展开前的屏幕位置(`headTop` 是点击捕获阶段记下的);
|
||||
* 2. 露出新展开的正文——正文底边在视口下方就补上超出量;正文比视口还高就对正文顶边,
|
||||
* 让用户从正文开头读起。
|
||||
*
|
||||
* 只补「刚好露出」的最小位移:正文比视口矮的时候,直接把正文滚进视口会把画面大幅上移,
|
||||
* 打断正在读历史的人。正文已经完全落在视口里(或已收起、高度归零)时就只冻头部。
|
||||
*/
|
||||
export type ConversationToggleRevealGeometry = {
|
||||
listRect: { top: number; bottom: number };
|
||||
headRect: { top: number; bottom: number };
|
||||
/** 折叠头在展开前相对视口顶边的位置。 */
|
||||
headTop: number;
|
||||
/** 折叠正文的几何;解析不出正文、或已收起传 null。 */
|
||||
bodyRect: { top: number; bottom: number; height: number } | null;
|
||||
};
|
||||
|
||||
export function toggleRevealDelta(
|
||||
input: ConversationToggleRevealGeometry,
|
||||
): number {
|
||||
const freeze = input.headRect.top - input.headTop;
|
||||
const body = input.bodyRect;
|
||||
if (!body || body.height <= 0) return freeze;
|
||||
|
||||
// 冻结之后再判正文位置:位移已经把正文整体上移了 freeze 像素。
|
||||
const bodyTop = body.top - freeze;
|
||||
const bodyBottom = body.bottom - freeze;
|
||||
const viewportHeight = input.listRect.bottom - input.listRect.top;
|
||||
if (body.height > viewportHeight && bodyBottom > input.listRect.bottom) {
|
||||
return freeze + Math.max(0, bodyTop - input.listRect.top);
|
||||
}
|
||||
if (bodyBottom > input.listRect.bottom) {
|
||||
return freeze + (bodyBottom - input.listRect.bottom);
|
||||
}
|
||||
return freeze;
|
||||
}
|
||||
|
||||
/**
|
||||
* 布局变化后把折叠头拉回展开前的位置;返回是否做了补偿。
|
||||
*
|
||||
@@ -65,25 +105,12 @@ export function freezeToggleHead(
|
||||
headRect.bottom > listRect.top && headRect.top < listRect.bottom;
|
||||
if (!headVisible) return false;
|
||||
|
||||
list.scrollTop += headRect.top - fold.headTop;
|
||||
revealExpandedBodyTop(list, fold.head);
|
||||
const body = resolveToggleBody(fold.head);
|
||||
list.scrollTop += toggleRevealDelta({
|
||||
listRect,
|
||||
headRect,
|
||||
headTop: fold.headTop,
|
||||
bodyRect: body?.getBoundingClientRect() ?? null,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开正文高于视口且底部超出视口时,把正文顶边对齐到视口顶边。
|
||||
*
|
||||
* 正文能在视口里放得下就不介入:那种情况下头部冻结已经足够,再动一次会把刚冻住的头部推走。
|
||||
*/
|
||||
function revealExpandedBodyTop(list: HTMLElement, head: HTMLElement): void {
|
||||
const body = resolveToggleBody(head);
|
||||
if (!body) return;
|
||||
|
||||
const listRect = list.getBoundingClientRect();
|
||||
const bodyRect = body.getBoundingClientRect();
|
||||
const viewportHeight = listRect.bottom - listRect.top;
|
||||
if (bodyRect.height <= viewportHeight) return;
|
||||
if (bodyRect.bottom <= listRect.bottom) return;
|
||||
|
||||
list.scrollTop += bodyRect.top - listRect.top;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user