fix(request-panel)/fixed overflow size column issue on first load (#8525)

* fix(request-panel)/fixed overflow size column issue on first load

* added tests

* refactor the total width calculation based on available space
This commit is contained in:
sachin-thakur-bruno
2026-07-08 12:43:53 +05:30
committed by GitHub
parent 86e4dc065b
commit 7af6150f42
2 changed files with 120 additions and 10 deletions

View File

@@ -35,20 +35,81 @@ const getSeparatorPositions = (widths) => {
return positions;
};
// Container too narrow for everyone to get minColWidth — split evenly instead
// (columns clip instead of overflow).
const distributeEqually = (count, total) => {
const each = Math.floor(total / count);
return [
...Array(count - 1).fill(each),
total - each * (count - 1)
];
};
// Pins any column that would shrink below minColWidth, then re-checks the
// rest (pinning one column shifts the math for the others). Returns the
// pinned widths plus what's left for the caller to hand out.
const pinColumnsAtMinimum = (widths, targetTotal, minColWidth) => {
const result = Array(widths.length).fill(null);
let remainingIdx = widths.map((_, i) => i);
let remainingTotal = targetTotal;
while (true) {
const remainingWidth = remainingIdx.reduce((sum, i) => sum + widths[i], 0);
const scale = remainingTotal / remainingWidth;
const nextRemaining = [];
for (const i of remainingIdx) {
if (widths[i] * scale < minColWidth) {
result[i] = minColWidth;
remainingTotal -= minColWidth;
} else {
nextRemaining.push(i);
}
}
if (nextRemaining.length === remainingIdx.length) {
return { result, remainingIdx, remainingTotal };
}
remainingIdx = nextRemaining;
}
};
// Largest remainder method: scales proportionally, floors each share, then
// gives the leftover whole pixels to whoever rounded down the most, so the
// sum always hits `total` exactly, with no single column eating the error.
const scaleAndRound = (widths, total) => {
const widthSum = widths.reduce((a, b) => a + b, 0);
const exact = widths.map((w) => (w / widthSum) * total);
const rounded = exact.map(Math.floor);
const leftover = total - rounded.reduce((a, b) => a + b, 0);
exact
.map((value, i) => ({ i, remainder: value - rounded[i] }))
.sort((a, b) => b.remainder - a.remainder)
.slice(0, leftover)
.forEach(({ i }) => rounded[i]++);
return rounded;
};
const scaleWidthsToTotal = (widths, targetTotal, minColWidth) => {
// When the container is too narrow to satisfy minColWidth for every column,
// fall back to equal distribution so the total stays exact (columns clip instead of overflow).
if (targetTotal < minColWidth * widths.length) {
const each = Math.floor(targetTotal / widths.length);
const last = targetTotal - each * (widths.length - 1);
return [...Array(widths.length - 1).fill(each), last];
return distributeEqually(widths.length, targetTotal);
}
const currentTotal = widths.reduce((s, w) => s + w, 0);
const factor = targetTotal / currentTotal;
const next = widths.slice(0, -1).map((w) => Math.max(minColWidth, Math.round(w * factor)));
const last = Math.max(minColWidth, targetTotal - next.reduce((s, w) => s + w, 0));
return [...next, last];
const { result, remainingIdx, remainingTotal }
= pinColumnsAtMinimum(widths, targetTotal, minColWidth);
const scaled = scaleAndRound(
remainingIdx.map((i) => widths[i]),
remainingTotal
);
remainingIdx.forEach((colIndex, i) => {
result[colIndex] = scaled[i];
});
return result;
};
export function useResizableColumns({ defaultWidths, initialWidths = null, minColWidth = 60, onResizeEnd = null }) {

View File

@@ -138,6 +138,55 @@ describe('useResizableColumns', () => {
});
});
describe('scaling invariants (regression: Devtools network tab "Size" column overflow)', () => {
const NETWORK_TAB_WIDTHS = [80, 70, 180, 300, 110, 100, 80];
const assertNoOverflowAndRespectsMin = (widths, minColWidth) => {
setup({ defaultWidths: widths, minColWidth });
const from = minColWidth * widths.length;
const to = Math.ceil(widths.reduce((s, w) => s + w, 0) * 1.5);
// Step by at least 2px: the hook itself treats a <=1px delta as noise
// and skips recomputing (see the ResizeObserver callback in index.js),
// which is unrelated to the scaling algorithm under test here.
const step = Math.max(2, Math.floor((to - from) / 200));
for (let target = from; target <= to; target += step) {
act(() => { triggerResize(target); });
const total = hookValue.colWidths.reduce((s, w) => s + w, 0);
const minVal = Math.min(...hookValue.colWidths);
expect(total).toBe(target);
expect(minVal).toBeGreaterThanOrEqual(minColWidth);
}
};
it('never overflows or dips under minColWidth across a sweep of container widths (real network-tab columns)', () => {
assertNoOverflowAndRespectsMin(NETWORK_TAB_WIDTHS, 60);
});
it('never overflows or dips under minColWidth across a sweep of container widths (generic columns)', () => {
assertNoOverflowAndRespectsMin(DEFAULT_WIDTHS, MIN_COL_WIDTH);
});
it('exact reproduction: shrinking to 380px (below minColWidth*7=420) still clips instead of overflowing', () => {
setup({ defaultWidths: NETWORK_TAB_WIDTHS, minColWidth: 60 });
act(() => { triggerResize(380); });
const total = hookValue.colWidths.reduce((s, w) => s + w, 0);
expect(total).toBe(380);
});
it('handles evenly-weighted columns without rounding pile-up pushing a column under minColWidth', () => {
// With naive per-column Math.round(), 4 equal-weight columns at this
// exact target used to round 3 columns up to minColWidth and leave the
// 4th short of it.
assertNoOverflowAndRespectsMin([20, 20, 20, 20], 10);
});
});
describe('drag resize', () => {
beforeEach(() => {
setup();