fix(editor): Keep NDV panels spanning the full container width (#35818)

This commit is contained in:
Daria
2026-08-07 13:12:16 +03:00
committed by GitHub
parent 897ce6ab9a
commit 561af4c567
2 changed files with 89 additions and 10 deletions
@@ -51,8 +51,9 @@ describe('useNdvLayout', () => {
localStorage.setItem(key, JSON.stringify({ left: 0, main: 5, right: 0 }));
const { panelWidthPercentage } = useNdvLayout({ container, hasInputPanel, paneType });
expect(panelWidthPercentage.value.left).toBeCloseTo(12);
expect(panelWidthPercentage.value.right).toBeCloseTo(12);
expect(panelWidthPercentage.value.left).toBeGreaterThanOrEqual(12);
expect(panelWidthPercentage.value.right).toBeGreaterThanOrEqual(12);
expect(panelWidthPercentage.value.main).toBeCloseTo(36.8);
});
it('updates layout on resize (left)', () => {
@@ -109,4 +110,67 @@ describe('useNdvLayout', () => {
expect(panelWidthPercentage.value.main).toBeCloseTo(42);
expect(panelWidthPercentage.value.right).toBeCloseTo(29);
});
describe('when the stored layout cannot be used as-is', () => {
const totalOf = ({ left, main, right }: { left: number; main: number; right: number }) =>
left + main + right;
it('spans the full container when stored values fall below the minimums', () => {
containerWidth.value = 1317;
const key = `${LOCAL_STORAGE_NDV_PANEL_WIDTH}_REGULAR`;
localStorage.setItem(key, JSON.stringify({ left: 1, main: 1, right: 1 }));
const { panelWidthPercentage } = useNdvLayout({ container, hasInputPanel, paneType });
// Minimums alone only add up to 46% of the container, leaving the canvas visible behind.
expect(totalOf(panelWidthPercentage.value)).toBeCloseTo(100);
expect(panelWidthPercentage.value.left).toBeGreaterThanOrEqual((120 / 1317) * 100);
expect(panelWidthPercentage.value.right).toBeGreaterThanOrEqual((120 / 1317) * 100);
expect(panelWidthPercentage.value.main).toBeGreaterThanOrEqual((368 / 1317) * 100);
});
it('falls back to the defaults when the stored value is not usable', () => {
containerWidth.value = 1317;
const key = `${LOCAL_STORAGE_NDV_PANEL_WIDTH}_REGULAR`;
localStorage.setItem(key, JSON.stringify({ left: null, main: null, right: null }));
const { panelWidthPercentage } = useNdvLayout({ container, hasInputPanel, paneType });
expect(panelWidthPercentage.value.main).toBeCloseTo((420 / 1317) * 100);
expect(totalOf(panelWidthPercentage.value)).toBeCloseTo(100);
});
it('keeps a usable layout while the container is unmeasured', () => {
containerWidth.value = 0;
const { panelWidthPercentage } = useNdvLayout({ container, hasInputPanel, paneType });
expect(Object.values(panelWidthPercentage.value).every(Number.isFinite)).toBe(true);
expect(totalOf(panelWidthPercentage.value)).toBeCloseTo(100);
});
it('does not persist while the container is unmeasured', () => {
containerWidth.value = 0;
const spy = vi.spyOn(Storage.prototype, 'setItem');
const { onResizeEnd } = useNdvLayout({ container, hasInputPanel, paneType });
onResizeEnd();
expect(spy).not.toHaveBeenCalled();
spy.mockRestore();
});
it('keeps the left panel collapsed for "inputless" layouts', () => {
containerWidth.value = 1317;
hasInputPanel.value = false;
paneType.value = 'inputless';
const key = `${LOCAL_STORAGE_NDV_PANEL_WIDTH}_INPUTLESS`;
localStorage.setItem(key, JSON.stringify({ left: 0, main: 1, right: 1 }));
const { panelWidthPercentage } = useNdvLayout({ container, hasInputPanel, paneType });
expect(panelWidthPercentage.value.left).toBe(0);
expect(totalOf(panelWidthPercentage.value)).toBeCloseTo(100);
});
});
});
@@ -72,6 +72,12 @@ export function useNdvLayout(options: UseNdvLayoutOptions) {
}
});
const isUsablePanelSize = (size: NdvPanelsSize | null | undefined): size is NdvPanelsSize =>
!!size &&
Number.isFinite(size.left) &&
Number.isFinite(size.main) &&
Number.isFinite(size.right);
const safePanelWidth = ({ left, main, right }: { left: number; main: number; right: number }) => {
const hasInput = toValue(options.hasInputPanel);
const minLeft = hasInput ? minPanelWidthPercentage.value : 0;
@@ -85,32 +91,41 @@ export function useNdvLayout(options: UseNdvLayoutOptions) {
};
const total = newPanelWidth.left + newPanelWidth.main + newPanelWidth.right;
const sides = newPanelWidth.left + newPanelWidth.right;
if (total > 100) {
const overflow = total - 100;
// Panels must always span the container: distribute any difference across the
// side panels, otherwise a short total leaves the canvas showing through.
if (total !== 100 && sides > 0) {
const diff = 100 - total;
const leftShare = newPanelWidth.left / sides;
const trimLeft = (newPanelWidth.left / (newPanelWidth.left + newPanelWidth.right)) * overflow;
const trimRight = overflow - trimLeft;
newPanelWidth.left = Math.max(minLeft, newPanelWidth.left - trimLeft);
newPanelWidth.right = Math.max(minRight, newPanelWidth.right - trimRight);
newPanelWidth.left = Math.max(minLeft, newPanelWidth.left + diff * leftShare);
newPanelWidth.right = Math.max(minRight, newPanelWidth.right + diff * (1 - leftShare));
}
return newPanelWidth;
};
const persistPanelSize = () => {
// Before the container is measured the sizes are placeholders, not something
// the user chose — persisting them would overwrite their actual layout.
if (!containerWidth.value || !isUsablePanelSize(panelWidthPercentage.value)) return;
localStorage.setItem(localStorageKey.value, JSON.stringify(panelWidthPercentage.value));
};
const loadPanelSize = () => {
if (!containerWidth.value) return;
const storedPanelSizeString = localStorage.getItem(localStorageKey.value);
const defaultSize = defaultPanelSize.value;
if (storedPanelSizeString) {
const storedPanelSize = jsonParse<NdvPanelsSize>(storedPanelSizeString, {
fallbackValue: defaultSize,
});
panelWidthPercentage.value = safePanelWidth(storedPanelSize ?? defaultSize);
panelWidthPercentage.value = safePanelWidth(
isUsablePanelSize(storedPanelSize) ? storedPanelSize : defaultSize,
);
} else {
panelWidthPercentage.value = safePanelWidth(defaultSize);
}