fix: keep the window's buttons clear of the app's after a maximise, restore, snap or resize

This commit is contained in:
Linus Rath
2026-11-04 11:11:36 +01:00
parent fadef1aee6
commit 3f4a1365f8
2 changed files with 39 additions and 4 deletions
@@ -348,6 +348,32 @@ describe('the window controls over the app', () => {
expect(root.hasAttribute(CONTROLS_ATTRIBUTE)).toBe(false);
expect(rightInset()).toBe('');
});
describe('when the window changes size in one step', () => {
const startWidth = window.innerWidth;
const setWidth = (width: number) => Object.defineProperty(window, 'innerWidth', { value: width, configurable: true, writable: true });
afterEach(() => setWidth(startWidth));
// As Chromium does it: geometrychange with the new rect while the page still has
// its old width, then the page takes the new width and fires resize.
const resizeTo = (fake: EventTarget, width: number) => act(() => {
(fake as unknown as { getTitlebarAreaRect: () => object }).getTitlebarAreaRect = () => ({ x: 0, y: 0, width: width - 137, height: 56 });
fake.dispatchEvent(new Event('geometrychange'));
setWidth(width);
window.dispatchEvent(new Event('resize'));
});
it('keeps the room for the controls after a maximise, a restore, a snap and a shrink', () => {
setWidth(1366);
const fake = overlayAt(0, 1366 - 137);
render(<WindowControlsHost />);
expect(rightInset()).toBe('137px');
for (const width of [2048, 1366, 1024, 640, 1920]) {
resizeTo(fake, width);
expect(rightInset(), `at ${width} px`).toBe('137px');
}
});
});
});
describe('which rows hold the controls', () => {
+13 -4
View File
@@ -94,9 +94,14 @@ export function applyControlsArea(area: TitlebarArea | null, root: HTMLElement =
/**
* Calls `listener` now and whenever the overlay's size or visibility changes
* (`geometrychange`). Without the API it calls it once with null and listens
* to nothing, so a browser without it runs no extra code. Returns the
* unsubscribe.
* (`geometrychange`), and again when the page's width does (`resize`).
* Without the API it calls it once with null and listens to nothing, so a
* browser without it runs no extra code. Returns the unsubscribe.
*
* `geometrychange` comes before the page has its new width: after a maximise,
* restore, snap or drag the rect is the new one and innerWidth still the old,
* so the room worked out from the two is wrong (none at all after growing, more
* than the window after shrinking). The `resize` that follows has both.
*/
export function watchTitlebarArea(listener: (area: TitlebarArea | null) => void): () => void {
listener(readTitlebarArea());
@@ -104,7 +109,11 @@ export function watchTitlebarArea(listener: (area: TitlebarArea | null) => void)
if (!current) return () => {};
const onChange = () => listener(readTitlebarArea());
current.addEventListener('geometrychange', onChange);
return () => current.removeEventListener('geometrychange', onChange);
window.addEventListener('resize', onChange);
return () => {
current.removeEventListener('geometrychange', onChange);
window.removeEventListener('resize', onChange);
};
}
const ROW_CANDIDATES = '[data-window-top-row], [data-composer-header], header, div.h-14, div.min-h-14, div.h-12, div.h-16, div.min-h-16';