Keep desktop sidebar controls stable across panel changes (#2078)

* fix(app): stabilize desktop sidebar controls

Keep the left-window-controls toggle mounted across sidebar state changes so its position and tooltip state remain stable. Preserve browser placement and show an explicit Explorer close control only where native right-side controls do not provide one.

* fix(app): suppress inactive sidebar chrome
This commit is contained in:
Mohamed Boudra
2026-07-15 15:30:57 +08:00
committed by GitHub
parent 9c40cb7637
commit 77f6069ec1
10 changed files with 326 additions and 33 deletions
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
intersectWindowChromeCorners,
resolveHasOwnedWindowChromeObstruction,
resolveWindowChromeObstruction,
resolveWindowChromeSafeArea,
} from "@/utils/desktop-window";
@@ -49,4 +50,33 @@ describe("window chrome", () => {
expect(intersectWindowChromeCorners("top-right", "both")).toBe("top-right");
expect(intersectWindowChromeCorners("top-left", "top-right")).toBe("none");
});
it("reports an obstruction only when the surface owns its corner", () => {
const obstruction = {
topLeft: { width: 78, height: 45 },
topRight: { width: 140, height: 48 },
};
expect(
resolveHasOwnedWindowChromeObstruction({
obstruction,
corners: "top-left",
corner: "top-left",
}),
).toBe(true);
expect(
resolveHasOwnedWindowChromeObstruction({
obstruction,
corners: "top-left",
corner: "top-right",
}),
).toBe(false);
expect(
resolveHasOwnedWindowChromeObstruction({
obstruction: { topLeft: null, topRight: null },
corners: "both",
corner: "top-right",
}),
).toBe(false);
});
});
+36
View File
@@ -24,6 +24,8 @@ interface WindowChromeObstruction {
topRight: WindowChromeCornerObstruction | null;
}
export type WindowChromeCorner = "top-left" | "top-right";
type WindowChromeSafeAreaStyle = { height: number } | { paddingLeft: number; paddingRight: number };
const EMPTY_OBSTRUCTION: WindowChromeObstruction = { topLeft: null, topRight: null };
@@ -37,6 +39,29 @@ function windowChromeCornersFromFlags(topLeft: boolean, topRight: boolean): Wind
return "none";
}
export function windowChromeCornersInclude(
corners: WindowChromeCorners,
corner: WindowChromeCorner,
): boolean {
return corners === "both" || corners === corner;
}
export function resolveHasOwnedWindowChromeObstruction(input: {
obstruction: WindowChromeObstruction;
corners: WindowChromeCorners;
corner: WindowChromeCorner;
}): boolean {
if (!windowChromeCornersInclude(input.corners, input.corner)) return false;
return input.corner === "top-left"
? input.obstruction.topLeft !== null
: input.obstruction.topRight !== null;
}
export function useHasWindowChromeObstruction(corner: WindowChromeCorner): boolean {
const obstruction = useContext(WindowChromeContext);
return corner === "top-left" ? obstruction.topLeft !== null : obstruction.topRight !== null;
}
export function intersectWindowChromeCorners(
inherited: WindowChromeCorners,
declared: WindowChromeCorners,
@@ -210,6 +235,17 @@ export function useWindowChromeCorners(): WindowChromeCorners {
return useContext(WindowChromeCornersContext);
}
export function useOwnsWindowChromeCorner(corner: WindowChromeCorner): boolean {
const corners = useContext(WindowChromeCornersContext);
return windowChromeCornersInclude(corners, corner);
}
export function useHasOwnedWindowChromeObstruction(corner: WindowChromeCorner): boolean {
const obstruction = useContext(WindowChromeContext);
const corners = useContext(WindowChromeCornersContext);
return resolveHasOwnedWindowChromeObstruction({ obstruction, corners, corner });
}
type WindowChromeSafeAreaProps = ViewProps & {
placement: WindowChromeSafeAreaPlacement;
horizontalPadding?: number;