mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Recompute collapsible content overflow after resize
This commit is contained in:
parent
0cb39fb6dc
commit
d9024ba983
4 changed files with 71 additions and 4 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { disposeDomListeners, listenDomEscapeKey, listenOutsideDomEvent } from "../../../shared/dom/events.dom";
|
||||
import { observeElementResize } from "../../../shared/dom/resize-observer.measure";
|
||||
import { syncTextareaHeight } from "../../../shared/dom/textarea-autogrow.measure";
|
||||
|
||||
export function syncGoalObjectiveHeight(textarea: HTMLTextAreaElement | null): void {
|
||||
|
|
@ -21,8 +22,10 @@ export function observeGoalObjectiveOverflow(content: HTMLElement, onOverflowCha
|
|||
};
|
||||
update();
|
||||
frame = win.requestAnimationFrame(update);
|
||||
const disposeResizeObserver = observeElementResize(content, update);
|
||||
return () => {
|
||||
if (frame) win.cancelAnimationFrame(frame);
|
||||
disposeResizeObserver();
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import type { Ref, ComponentChild as UiNode } from "preact";
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { listenDomEvent, listenOutsideDomEvent } from "../../../../shared/dom/events.dom";
|
||||
import { disposeDomListeners, listenDomEvent, listenOutsideDomEvent } from "../../../../shared/dom/events.dom";
|
||||
import { observeElementResize } from "../../../../shared/dom/resize-observer.measure";
|
||||
import type { ThreadStreamTextView } from "../../presentation/thread-stream/text-view";
|
||||
import { THREAD_STREAM_CONTENT_RENDERED_EVENT } from "./content-rendered-event.dom";
|
||||
import type { TextItemContentContext } from "./context";
|
||||
|
|
@ -21,9 +22,10 @@ export function CollapsibleTextContent({ view, context }: { view: ThreadStreamTe
|
|||
setOverflows(content.scrollHeight > userDialogueCollapseHeight(content) + 1);
|
||||
};
|
||||
const disposeRendered = listenDomEvent(content, THREAD_STREAM_CONTENT_RENDERED_EVENT, update);
|
||||
const disposeResizeObserver = observeElementResize(content, update);
|
||||
update();
|
||||
content.win.requestAnimationFrame(update);
|
||||
return disposeRendered;
|
||||
return disposeDomListeners(disposeRendered, disposeResizeObserver);
|
||||
}, [view.id, view.body, view.renderMode]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
9
src/shared/dom/resize-observer.measure.ts
Normal file
9
src/shared/dom/resize-observer.measure.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export function observeElementResize(element: HTMLElement, onResize: () => void): () => void {
|
||||
const ResizeObserverCtor = (element.win as Window & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver;
|
||||
if (!ResizeObserverCtor) return () => undefined;
|
||||
const observer = new ResizeObserverCtor(onResize);
|
||||
observer.observe(element);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}
|
||||
|
|
@ -82,6 +82,31 @@ describe("GoalPanel", () => {
|
|||
expect(parent.querySelector<HTMLDetailsElement>(".codex-panel__goal-objective-collapse-details")?.hidden).toBe(true);
|
||||
});
|
||||
|
||||
it("recomputes objective overflow when its rendered size changes", async () => {
|
||||
const parent = document.createElement("div");
|
||||
let scrollHeight = 40;
|
||||
|
||||
await withResizeObserver(async (notifyResize) => {
|
||||
await withGoalObjectiveScrollHeight(
|
||||
() => scrollHeight,
|
||||
async () => {
|
||||
await act(async () => {
|
||||
renderGoal(parent, goal({ objective: "resizable" }), actions());
|
||||
});
|
||||
const details = expectPresent(parent.querySelector<HTMLDetailsElement>(".codex-panel__goal-objective-collapse-details"));
|
||||
expect(details.hidden).toBe(true);
|
||||
|
||||
scrollHeight = 100;
|
||||
await act(async () => {
|
||||
notifyResize();
|
||||
});
|
||||
|
||||
expect(details.hidden).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("saves inline edits from the editor frame icon while preserving the existing token budget", async () => {
|
||||
const parent = document.createElement("div");
|
||||
document.body.appendChild(parent);
|
||||
|
|
@ -324,12 +349,16 @@ async function outsidePointerDown(): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
async function withGoalObjectiveScrollHeight<T>(scrollHeight: number, fn: () => Promise<T>): Promise<T> {
|
||||
async function withGoalObjectiveScrollHeight<T>(scrollHeight: number | (() => number), fn: () => Promise<T>): Promise<T> {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "scrollHeight");
|
||||
Object.defineProperty(HTMLElement.prototype, "scrollHeight", {
|
||||
configurable: true,
|
||||
get() {
|
||||
return this.classList.contains("codex-panel__goal-objective") ? scrollHeight : 0;
|
||||
return this.classList.contains("codex-panel__goal-objective")
|
||||
? typeof scrollHeight === "function"
|
||||
? scrollHeight()
|
||||
: scrollHeight
|
||||
: 0;
|
||||
},
|
||||
});
|
||||
try {
|
||||
|
|
@ -343,6 +372,30 @@ async function withGoalObjectiveScrollHeight<T>(scrollHeight: number, fn: () =>
|
|||
}
|
||||
}
|
||||
|
||||
async function withResizeObserver<T>(fn: (notifyResize: () => void) => Promise<T>): Promise<T> {
|
||||
const original = window.ResizeObserver;
|
||||
const callbacks: (() => void)[] = [];
|
||||
class TestResizeObserver implements ResizeObserver {
|
||||
constructor(callback: ResizeObserverCallback) {
|
||||
callbacks.push(() => {
|
||||
callback([], this);
|
||||
});
|
||||
}
|
||||
|
||||
observe(): void {}
|
||||
unobserve(): void {}
|
||||
disconnect(): void {}
|
||||
}
|
||||
window.ResizeObserver = TestResizeObserver;
|
||||
try {
|
||||
return await fn(() => {
|
||||
for (const callback of callbacks) callback();
|
||||
});
|
||||
} finally {
|
||||
window.ResizeObserver = original;
|
||||
}
|
||||
}
|
||||
|
||||
async function textareaKeydown(
|
||||
parent: HTMLElement,
|
||||
options: { key: string; metaKey?: boolean; ctrlKey?: boolean; shiftKey?: boolean; altKey?: boolean; isComposing?: boolean },
|
||||
|
|
|
|||
Loading…
Reference in a new issue