mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(studio): keep the shown tab in view when its strip narrows
Focusing a panel draws its strip actions, which could hide the shown tab of a strip that only just fit. The load smoke checks move into their own function to keep the smoke runner simple.
This commit is contained in:
@@ -11,11 +11,23 @@ import { PANEL_IDS, type PanelId } from "./panelRegistry";
|
||||
|
||||
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
// happy-dom never resizes anything; a test calls `resizeAll` to stand in for a real resize.
|
||||
const liveObservers = new Set<ResizeObserverStub>();
|
||||
class ResizeObserverStub {
|
||||
readonly callback: () => void;
|
||||
constructor(callback: () => void) {
|
||||
this.callback = callback;
|
||||
liveObservers.add(this);
|
||||
}
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
disconnect() {
|
||||
liveObservers.delete(this);
|
||||
}
|
||||
}
|
||||
const resizeAll = () => {
|
||||
for (const observer of liveObservers) observer.callback();
|
||||
};
|
||||
(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = ResizeObserverStub;
|
||||
|
||||
// happy-dom has no layout: a tab sits at 100px per position and is 90px wide.
|
||||
@@ -160,4 +172,15 @@ describe("dock tab fill", () => {
|
||||
expect(stripOf("design")?.querySelectorAll(".hf-dock-tab-fill")).toHaveLength(1);
|
||||
expect(fill()?.style.transform).toBe(`translateX(${2 * TAB_STEP}px)`);
|
||||
});
|
||||
|
||||
it("keeps the shown tab whole when its strip narrows, as when the strip's actions appear", async () => {
|
||||
const strip = stripOf("design");
|
||||
if (!strip) throw new Error("no design strip");
|
||||
await activate("variables");
|
||||
// The four tabs fit, so the strip is unscrolled.
|
||||
strip.scrollLeft = 0;
|
||||
Object.defineProperty(strip, "clientWidth", { value: 250, configurable: true });
|
||||
act(() => resizeAll());
|
||||
expect(strip.scrollLeft).toBe(3 * TAB_STEP + TAB_WIDTH + 1 - 250);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,16 +15,26 @@ function fillOf(list: HTMLElement): HTMLElement {
|
||||
return fill;
|
||||
}
|
||||
|
||||
/** The scroll that shows the whole shown tab, e.g. after the strip narrows because its actions appeared. */
|
||||
function revealedScroll(box: { left: number; width: number }, scrollLeft: number, width: number) {
|
||||
if (box.left < scrollLeft) return box.left;
|
||||
// Offset sizes are rounded while the tab's edge is fractional; a pixel over is clamped at the end.
|
||||
return Math.max(scrollLeft, box.left + box.width + 1 - width);
|
||||
}
|
||||
|
||||
/** Reads where the shown tab sits; the write happens after every strip is read, so layout runs once. */
|
||||
function measureFill(list: HTMLElement): () => void {
|
||||
const fill = fillOf(list);
|
||||
const tab = list.querySelector<HTMLElement>(":scope > .dv-active-tab");
|
||||
const box = tab ? { width: tab.offsetWidth, left: tab.offsetLeft } : null;
|
||||
const scrollLeft = list.scrollLeft;
|
||||
const target = box ? revealedScroll(box, scrollLeft, list.clientWidth) : scrollLeft;
|
||||
return () => {
|
||||
fill.hidden = !box;
|
||||
if (!box) return;
|
||||
fill.style.width = `${box.width}px`;
|
||||
fill.style.transform = `translateX(${box.left}px)`;
|
||||
if (target !== scrollLeft) list.scrollLeft = target;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,7 +49,8 @@ function markClippedEdges(list: HTMLElement) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Slides one fill per tab strip under its shown tab, and keeps the clipped-edge marks current.
|
||||
* Slides one fill per tab strip under its shown tab, keeps that tab in view, and keeps the
|
||||
* clipped-edge marks current.
|
||||
* Returns a disposer.
|
||||
*/
|
||||
export function installTabFill(api: DockviewApi, root: HTMLElement): () => void {
|
||||
|
||||
@@ -114,6 +114,33 @@ function clippedStrip(index, allowance) {
|
||||
return `${labels.join(", ")}: ${strip.scrollWidth}px of tabs in ${Math.floor(room)}px`;
|
||||
}
|
||||
|
||||
/** Every dock strip gives its tabs the whole width it can, and fits them while its group is active. */
|
||||
async function dockStripErrors(page) {
|
||||
const found = [];
|
||||
const reservedSlots = await page.$$eval(
|
||||
".dv-groupview.dv-inactive-group .dv-right-actions-container",
|
||||
(slots) => slots.filter((slot) => slot.getBoundingClientRect().width > 0).length,
|
||||
);
|
||||
if (reservedSlots > 0) {
|
||||
found.push(`${reservedSlots} inactive dock strips hold width for actions they do not draw`);
|
||||
}
|
||||
const shownTabs = await page.$$(".dv-tabs-container .dv-active-tab");
|
||||
for (const [index, tab] of shownTabs.entries()) {
|
||||
// An active group draws its strip actions, so this is the least room its tabs get.
|
||||
await tab.click();
|
||||
await page.evaluate(
|
||||
() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))),
|
||||
);
|
||||
const clipped = await page.evaluate(clippedStrip, index, MAC_FONT_ALLOWANCE_PX);
|
||||
if (clipped) {
|
||||
found.push(
|
||||
`Dock tab strip clips a label at the default layout (${MAC_FONT_ALLOWANCE_PX}px macOS allowance): ${clipped}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
export function isExpectedStudioSmokeError(message) {
|
||||
return message.includes("favicon.ico");
|
||||
}
|
||||
@@ -162,27 +189,7 @@ export async function runStudioRuntimeSmoke(targetUrl) {
|
||||
return textContent.includes("Something went wrong") ? textContent : null;
|
||||
});
|
||||
if (errorBoundary) errors.push(`React error boundary triggered: ${errorBoundary}`);
|
||||
const reservedSlots = await page.$$eval(
|
||||
".dv-groupview.dv-inactive-group .dv-right-actions-container",
|
||||
(slots) => slots.filter((slot) => slot.getBoundingClientRect().width > 0).length,
|
||||
);
|
||||
if (reservedSlots > 0) {
|
||||
errors.push(`${reservedSlots} inactive dock strips hold width for actions they do not draw`);
|
||||
}
|
||||
const shownTabs = await page.$$(".dv-tabs-container .dv-active-tab");
|
||||
for (const [index, tab] of shownTabs.entries()) {
|
||||
// An active group draws its strip actions, so this is the least room its tabs get.
|
||||
await tab.click();
|
||||
await page.evaluate(
|
||||
() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))),
|
||||
);
|
||||
const clipped = await page.evaluate(clippedStrip, index, MAC_FONT_ALLOWANCE_PX);
|
||||
if (clipped) {
|
||||
errors.push(
|
||||
`Dock tab strip clips a label at the default layout (${MAC_FONT_ALLOWANCE_PX}px macOS allowance): ${clipped}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
errors.push(...(await dockStripErrors(page)));
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user