mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
perf(ui): keep split divider drag within frame budget (#127840)
* perf(ui): keep split resize work within frames * test(ui): prove shared divider drag persistence
This commit is contained in:
committed by
GitHub
parent
70c2be7e6a
commit
f70947bf61
@@ -233,14 +233,18 @@ describe("resizable-divider", () => {
|
||||
expect([...divider.classList]).toEqual(["dragging"]);
|
||||
|
||||
dispatchPointer(document, "pointermove", 220, 7);
|
||||
expectLastResizeRatio(resized, 0.7);
|
||||
dispatchPointer(document, "pointermove", 120, 7);
|
||||
expect(resized).not.toHaveBeenCalled();
|
||||
await nextFrame();
|
||||
expectLastResizeRatio(resized, 0.65);
|
||||
expect(resized).toHaveBeenCalledTimes(1);
|
||||
expect(resizeEnded).not.toHaveBeenCalled();
|
||||
|
||||
dispatchPointer(document, "pointerup", 220, 7);
|
||||
const endEvent = resizeEnded.mock.lastCall?.[0] as
|
||||
| CustomEvent<{ splitRatio: number }>
|
||||
| undefined;
|
||||
expect(endEvent?.detail).toEqual({ splitRatio: 0.7 });
|
||||
expect(endEvent?.detail).toEqual({ splitRatio: 0.65 });
|
||||
expect(resizeEnded).toHaveBeenCalledTimes(1);
|
||||
expect([...divider.classList]).toEqual([]);
|
||||
expect(releasePointerCapture).toHaveBeenCalledWith(7);
|
||||
@@ -264,4 +268,21 @@ describe("resizable-divider", () => {
|
||||
dispatchPointer(document, "pointermove", 220);
|
||||
expect(resized).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("commits the final pointer position when disconnected", async () => {
|
||||
const divider = await renderDivider();
|
||||
const resized = vi.fn();
|
||||
const resizeEnded = vi.fn();
|
||||
divider.setPointerCapture = vi.fn();
|
||||
divider.releasePointerCapture = vi.fn();
|
||||
divider.addEventListener("resize", resized);
|
||||
divider.addEventListener("resize-end", resizeEnded);
|
||||
|
||||
dispatchPointer(divider, "pointerdown", 100);
|
||||
dispatchPointer(document, "pointermove", 120);
|
||||
divider.remove();
|
||||
|
||||
expectLastResizeRatio(resized, 0.65);
|
||||
expectLastResizeRatio(resizeEnded, 0.65);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,9 @@ class ResizableDivider extends OpenClawLitElement {
|
||||
private startPosition = 0;
|
||||
private startRatio = 0;
|
||||
private dragRatio = 0;
|
||||
private dragSize = 0;
|
||||
private dragFrame = 0;
|
||||
private pendingPosition: number | null = null;
|
||||
private activePointerId: number | null = null;
|
||||
|
||||
static override styles = css`
|
||||
@@ -114,7 +117,7 @@ class ResizableDivider extends OpenClawLitElement {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("pointerdown", this.handlePointerDown);
|
||||
this.removeEventListener("keydown", this.handleKeyDown);
|
||||
this.stopDragging();
|
||||
this.finishDragging(new Event("disconnect"));
|
||||
}
|
||||
|
||||
protected override updated() {
|
||||
@@ -132,6 +135,10 @@ class ResizableDivider extends OpenClawLitElement {
|
||||
this.startPosition = this.orientation === "horizontal" ? e.clientY : e.clientX;
|
||||
this.startRatio = this.currentRatio();
|
||||
this.dragRatio = this.startRatio;
|
||||
this.dragSize = this.measureDragSize();
|
||||
if (this.dragSize <= 0) {
|
||||
return;
|
||||
}
|
||||
this.classList.add("dragging");
|
||||
this.capturePointer(e.pointerId);
|
||||
|
||||
@@ -148,35 +155,21 @@ class ResizableDivider extends OpenClawLitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = this.parentElement;
|
||||
if (!container) {
|
||||
return;
|
||||
this.pendingPosition = this.orientation === "horizontal" ? e.clientY : e.clientX;
|
||||
if (!this.dragFrame) {
|
||||
this.dragFrame = requestAnimationFrame(this.flushPointerMove);
|
||||
}
|
||||
};
|
||||
|
||||
// Ratio is local to the two adjacent siblings, not the whole container:
|
||||
// split-view rows/columns hold N panes, and a drag must only redistribute
|
||||
// the pair this divider sits between. Container size is the 2-child
|
||||
// fallback (legacy chat sidebar split).
|
||||
const previousBounds = this.previousElementSibling?.getBoundingClientRect();
|
||||
const nextBounds = this.nextElementSibling?.getBoundingClientRect();
|
||||
const containerBounds = container.getBoundingClientRect();
|
||||
const measuredSize = this.measureSize?.() ?? 0;
|
||||
const siblingSize =
|
||||
this.orientation === "horizontal"
|
||||
? (previousBounds?.height ?? 0) + (nextBounds?.height ?? 0)
|
||||
: (previousBounds?.width ?? 0) + (nextBounds?.width ?? 0);
|
||||
const containerSize =
|
||||
measuredSize > 0
|
||||
? measuredSize
|
||||
: siblingSize ||
|
||||
(this.orientation === "horizontal" ? containerBounds.height : containerBounds.width);
|
||||
if (containerSize <= 0) {
|
||||
return;
|
||||
private readonly flushPointerMove = () => {
|
||||
this.dragFrame = 0;
|
||||
const position = this.pendingPosition;
|
||||
this.pendingPosition = null;
|
||||
if (position !== null) {
|
||||
this.dragRatio = this.emitResize(
|
||||
this.startRatio + (position - this.startPosition) / this.dragSize,
|
||||
);
|
||||
}
|
||||
const position = this.orientation === "horizontal" ? e.clientY : e.clientX;
|
||||
const deltaRatio = (position - this.startPosition) / containerSize;
|
||||
|
||||
this.dragRatio = this.emitResize(this.startRatio + deltaRatio);
|
||||
};
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
@@ -210,6 +203,11 @@ class ResizableDivider extends OpenClawLitElement {
|
||||
return;
|
||||
}
|
||||
if (this.activePointerId !== null) {
|
||||
if (this.dragFrame) {
|
||||
cancelAnimationFrame(this.dragFrame);
|
||||
this.dragFrame = 0;
|
||||
}
|
||||
this.flushPointerMove();
|
||||
this.emitResizeEnd(this.dragRatio);
|
||||
}
|
||||
this.stopDragging();
|
||||
@@ -222,6 +220,11 @@ class ResizableDivider extends OpenClawLitElement {
|
||||
}
|
||||
this.classList.remove("dragging");
|
||||
this.releaseActivePointer(pointerId);
|
||||
if (this.dragFrame) {
|
||||
cancelAnimationFrame(this.dragFrame);
|
||||
this.dragFrame = 0;
|
||||
}
|
||||
this.pendingPosition = null;
|
||||
|
||||
window.removeEventListener("pointermove", this.handlePointerMove);
|
||||
for (const type of DRAG_END_EVENTS) {
|
||||
@@ -256,6 +259,26 @@ class ResizableDivider extends OpenClawLitElement {
|
||||
return Math.max(this.minRatio, Math.min(this.maxRatio, value));
|
||||
}
|
||||
|
||||
private measureDragSize() {
|
||||
const measuredSize = this.measureSize?.() ?? 0;
|
||||
if (measuredSize > 0) {
|
||||
return measuredSize;
|
||||
}
|
||||
const previousBounds = this.previousElementSibling?.getBoundingClientRect();
|
||||
const nextBounds = this.nextElementSibling?.getBoundingClientRect();
|
||||
const siblingSize =
|
||||
this.orientation === "horizontal"
|
||||
? (previousBounds?.height ?? 0) + (nextBounds?.height ?? 0)
|
||||
: (previousBounds?.width ?? 0) + (nextBounds?.width ?? 0);
|
||||
if (siblingSize > 0) {
|
||||
return siblingSize;
|
||||
}
|
||||
const containerBounds = this.parentElement?.getBoundingClientRect();
|
||||
return this.orientation === "horizontal"
|
||||
? (containerBounds?.height ?? 0)
|
||||
: (containerBounds?.width ?? 0);
|
||||
}
|
||||
|
||||
private currentRatio() {
|
||||
const measuredRatio = this.measureRatio?.();
|
||||
return measuredRatio !== undefined && Number.isFinite(measuredRatio)
|
||||
|
||||
@@ -391,16 +391,16 @@ suite.define(() => {
|
||||
const divider = page.locator(".board-session-surface__divider");
|
||||
const dock = page.locator(".board-session-surface__chat");
|
||||
const dockHeight = () => dock.evaluate((element) => getComputedStyle(element).height);
|
||||
await divider.focus();
|
||||
await page.keyboard.press("End");
|
||||
const dividerBounds = await divider.boundingBox();
|
||||
expect(dividerBounds).not.toBeNull();
|
||||
await page.mouse.move(
|
||||
dividerBounds!.x + dividerBounds!.width / 2,
|
||||
dividerBounds!.y + dividerBounds!.height / 2,
|
||||
);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(dividerBounds!.x, dividerBounds!.y - 80);
|
||||
await page.mouse.up();
|
||||
await expect.poll(dockHeight).not.toBe("320px");
|
||||
const clampedHeight = await dockHeight();
|
||||
// End pins the bottom dock against its clamp, so step back off it: comparing
|
||||
// a clamped height to itself after reload would pass if persistence broke and
|
||||
// the dock merely fell back to its minimum.
|
||||
await page.keyboard.press("ArrowUp");
|
||||
await page.keyboard.press("ArrowUp");
|
||||
await expect.poll(dockHeight).not.toBe(clampedHeight);
|
||||
const persistedHeight = await dockHeight();
|
||||
expect(persistedHeight).toMatch(/^\d+(?:\.\d+)?px$/u);
|
||||
|
||||
|
||||
@@ -669,6 +669,18 @@ describe("chat page split layout host", () => {
|
||||
expect(panes.every((pane) => pane.onOpenSplitView === undefined)).toBe(true);
|
||||
expect(panes[0]?.chatMessagesBySession).toBe(panes[1]?.chatMessagesBySession);
|
||||
|
||||
itemAt(dividers, 0, "split divider").dispatchEvent(
|
||||
new CustomEvent("resize", { detail: { splitRatio: 0.7 } }),
|
||||
);
|
||||
await page.updateComplete;
|
||||
expect(getLayout(page)?.columnWeights[0]).toBeCloseTo(0.7);
|
||||
expect(getLayout(page)?.columnWeights[1]).toBeCloseTo(0.3);
|
||||
expect(loadSettings().chatSplitLayout).toBeUndefined();
|
||||
|
||||
itemAt(dividers, 0, "split divider").dispatchEvent(new CustomEvent("resize-end"));
|
||||
expect(loadSettings().chatSplitLayout?.columnWeights[0]).toBeCloseTo(0.7);
|
||||
expect(loadSettings().chatSplitLayout?.columnWeights[1]).toBeCloseTo(0.3);
|
||||
|
||||
itemAt(cells, 0, "split cell").dispatchEvent(new Event("pointerdown"));
|
||||
await page.updateComplete;
|
||||
|
||||
|
||||
@@ -647,13 +647,16 @@ export class ChatPage extends OpenClawLightDomElement {
|
||||
.maxRatio=${0.85}
|
||||
.label=${t("nav.resize")}
|
||||
@resize=${(event: CustomEvent<{ splitRatio: number }>) => {
|
||||
const current = this.layout;
|
||||
if (current) {
|
||||
this.persistLayout(
|
||||
resizePanes(current, column.id, paneIndex, event.detail.splitRatio),
|
||||
);
|
||||
}
|
||||
this.layout = this.layout
|
||||
? resizePanes(
|
||||
this.layout,
|
||||
column.id,
|
||||
paneIndex,
|
||||
event.detail.splitRatio,
|
||||
)
|
||||
: undefined;
|
||||
}}
|
||||
@resize-end=${() => this.persistLayout(this.layout)}
|
||||
></resizable-divider>
|
||||
`
|
||||
: nothing}
|
||||
@@ -672,13 +675,11 @@ export class ChatPage extends OpenClawLightDomElement {
|
||||
.maxRatio=${0.85}
|
||||
.label=${t("nav.resize")}
|
||||
@resize=${(event: CustomEvent<{ splitRatio: number }>) => {
|
||||
const current = this.layout;
|
||||
if (current) {
|
||||
this.persistLayout(
|
||||
resizeColumns(current, columnIndex, event.detail.splitRatio),
|
||||
);
|
||||
}
|
||||
this.layout = this.layout
|
||||
? resizeColumns(this.layout, columnIndex, event.detail.splitRatio)
|
||||
: undefined;
|
||||
}}
|
||||
@resize-end=${() => this.persistLayout(this.layout)}
|
||||
></resizable-divider>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
Reference in New Issue
Block a user