fix(studio): reveal the shown tab only when its strip narrows

A strip the user scrolled by hand no longer snaps back when another panel
changes. The load smoke measures the focused group's strip directly.
This commit is contained in:
Miguel Ángel
2026-09-23 11:00:36 -04:00
parent 2d550eb35c
commit 6a5edd893a
3 changed files with 36 additions and 10 deletions
@@ -177,10 +177,25 @@ describe("dock tab fill", () => {
const strip = stripOf("design");
if (!strip) throw new Error("no design strip");
await activate("variables");
// The four tabs fit, so the strip is unscrolled.
// The four tabs fit, so the strip is unscrolled; then it narrows to two and a half tabs.
strip.scrollLeft = 0;
Object.defineProperty(strip, "clientWidth", { value: 1000, configurable: true });
act(() => resizeAll());
Object.defineProperty(strip, "clientWidth", { value: 250, configurable: true });
act(() => resizeAll());
expect(strip.scrollLeft).toBe(3 * TAB_STEP + TAB_WIDTH + 1 - 250);
});
it("leaves a strip the user scrolled alone when something else in the dock changes", async () => {
const strip = stripOf("design");
if (!strip) throw new Error("no design strip");
await activate("variables");
Object.defineProperty(strip, "clientWidth", { value: 250, configurable: true });
act(() => resizeAll());
// Scrolled back by hand to look at the first tabs, then another group takes focus.
strip.scrollLeft = 0;
await activate("assets");
act(() => resizeAll());
expect(strip.scrollLeft).toBe(0);
});
});
@@ -22,13 +22,16 @@ function revealedScroll(box: { left: number; width: number }, scrollLeft: number
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 {
/**
* Reads where the shown tab sits; the write happens after every strip is read, so layout runs once.
* Only a strip that got narrower is scrolled, so a strip the user scrolled by hand stays put.
*/
function measureFill(list: HTMLElement, narrowed: boolean): () => 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;
const target = box && narrowed ? revealedScroll(box, scrollLeft, list.clientWidth) : scrollLeft;
return () => {
fill.hidden = !box;
if (!box) return;
@@ -70,9 +73,16 @@ export function installTabFill(api: DockviewApi, root: HTMLElement): () => void
observed.add(element);
}
}
const widths = new WeakMap<HTMLElement, number>();
function narrowed(list: HTMLElement) {
const width = list.clientWidth;
const before = widths.get(list) ?? width;
widths.set(list, width);
return width < before;
}
function placeAll() {
const strips = [...lists()];
const writes = strips.map(measureFill);
const writes = strips.map((list) => measureFill(list, narrowed(list)));
for (const write of writes) write();
for (const list of strips) markClippedEdges(list);
observe(strips.flatMap((list) => [list, ...list.querySelectorAll(":scope > .dv-tab")]));
+6 -5
View File
@@ -101,9 +101,10 @@ export function studioSmokeApiResponse(method, requestUrl) {
// about 10px narrower than macOS does; keep that much room so a strip that fits here fits on a Mac.
const MAC_FONT_ALLOWANCE_PX = 10;
/** Runs in the page: the strip's tabs, plus the allowance, must fit before its action slot. */
function clippedStrip(index, allowance) {
const strip = document.querySelectorAll(".dv-tabs-container")[index];
/** Runs in the page: the active group's tabs, plus the allowance, must fit before its actions. */
function clippedStrip(allowance) {
const strip = document.querySelector(".dv-groupview.dv-active-group .dv-tabs-container");
if (!strip) return "no active dock strip";
const actions = strip
.closest(".dv-tabs-and-actions-container")
?.querySelector(".dv-right-actions-container");
@@ -125,13 +126,13 @@ async function dockStripErrors(page) {
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()) {
for (const tab of shownTabs) {
// 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);
const clipped = await page.evaluate(clippedStrip, 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}`,