mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
fix(zoom) desktop chrome clearance under text zoom (#1490)
Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
@@ -50,6 +50,7 @@ export default defineConfig({
|
||||
"**/unread-pill.spec.ts",
|
||||
"**/sidebar-more-unread-overlap.spec.ts",
|
||||
"**/home-collapsed-top-chrome.spec.ts",
|
||||
"**/top-chrome-zoom-clearance.spec.ts",
|
||||
"**/thread-unread.spec.ts",
|
||||
"**/workspace-rail.spec.ts",
|
||||
"**/thread-reply-anchor-roleplay.spec.ts",
|
||||
|
||||
@@ -21,10 +21,14 @@ type AppTopChromeProps = {
|
||||
hasWorkspaceRail?: boolean;
|
||||
};
|
||||
|
||||
// Fixed px on purpose (button box + glyph): these controls sit beside the
|
||||
// native macOS traffic lights, which ignore the app's Cmd +/- text zoom, so
|
||||
// the row must not grow or shrink with the rem scale. Deliberate exception
|
||||
// to the rem-first rule.
|
||||
const TOP_CHROME_ICON_BUTTON_CLASS =
|
||||
"h-7 w-7 rounded-[4px] text-sidebar-foreground/65 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&_svg]:size-4";
|
||||
"h-[28px] w-[28px] rounded-[4px] text-sidebar-foreground/65 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&_svg]:size-[16px]";
|
||||
const HISTORY_ICON_BUTTON_CLASS =
|
||||
"h-7 w-6 rounded-[4px] text-sidebar-foreground/65 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&_svg]:size-4";
|
||||
"h-[28px] w-[24px] rounded-[4px] text-sidebar-foreground/65 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&_svg]:size-[16px]";
|
||||
|
||||
function preventTopChromeWheel(event: WheelEvent) {
|
||||
event.preventDefault();
|
||||
@@ -66,11 +70,15 @@ export function AppTopChrome({
|
||||
// x-position. When the workspace rail is present it already occupies the far
|
||||
// left, so the nav row only needs to clear the lights past the rail edge
|
||||
// rather than the full offset. In fullscreen those buttons hide.
|
||||
//
|
||||
// Fixed px on purpose: the native traffic lights do not scale with the app's
|
||||
// Cmd +/- text zoom (rem), so rem-based clearance shrinks under them when
|
||||
// zoomed out. This is a deliberate exception to the rem-first rule.
|
||||
const macChrome = isMacPlatform() && !isFullscreen;
|
||||
const navRowPaddingClass = macChrome
|
||||
? hasWorkspaceRail
|
||||
? "pl-8"
|
||||
: "pl-20"
|
||||
? "pl-[32px]"
|
||||
: "pl-[80px]"
|
||||
: "pl-3";
|
||||
const navRowAlignmentClass = macChrome ? "translate-y-[3px]" : null;
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ export function WorkspaceRail({
|
||||
// macOS traffic lights overlay the top-left, so start buttons below them (they hide in fullscreen).
|
||||
const topPaddingClass =
|
||||
isMacPlatform() && !isFullscreen
|
||||
? "pt-(--buzz-top-chrome-height,2.5rem)"
|
||||
? "pt-(--buzz-top-chrome-height,40px)"
|
||||
: "pt-3";
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
export const TOP_CHROME_HEIGHT_DEFAULT = "2.5rem";
|
||||
// Fixed px on purpose: the top chrome strip hosts the fixed-size macOS
|
||||
// traffic lights and the px-pinned nav buttons (see `AppTopChrome`), so its
|
||||
// height must not follow the Cmd +/- rem text scale either. Deliberate
|
||||
// exception to the rem-first rule. 40px == the old 2.5rem at default zoom.
|
||||
export const TOP_CHROME_HEIGHT_DEFAULT = "40px";
|
||||
export const CHANNEL_CONTENT_TOP_PADDING_DEFAULT = "5.75rem";
|
||||
|
||||
export const chromeCssVars = {
|
||||
@@ -38,9 +42,9 @@ export const topChromeInset = {
|
||||
/** Tailwind class fragments for the global top chrome backdrop strip. */
|
||||
export const topChromeBackdrop = {
|
||||
/** Height matching the global top chrome search/drag strip. */
|
||||
height: "h-(--buzz-top-chrome-height,2.5rem)",
|
||||
height: "h-(--buzz-top-chrome-height,40px)",
|
||||
/** `after:` pseudo-element offset aligned to the bottom of top chrome. */
|
||||
dividerTop: "after:top-(--buzz-top-chrome-height,2.5rem)",
|
||||
dividerTop: "after:top-(--buzz-top-chrome-height,40px)",
|
||||
} as const;
|
||||
|
||||
/** Tailwind class fragments for measured channel header chrome. */
|
||||
|
||||
138
desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts
Normal file
138
desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
|
||||
// The macOS traffic lights are native chrome: with `trafficLightPosition`
|
||||
// x:16 they occupy roughly x 16–68 regardless of the app's Cmd +/- text
|
||||
// zoom. The top-chrome nav row must clear that band in fixed px, so the
|
||||
// clearance cannot shrink when the root font size scales down.
|
||||
const TRAFFIC_LIGHT_RIGHT_EDGE = 72;
|
||||
|
||||
async function spoofMacPlatform(page: import("@playwright/test").Page) {
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(navigator, "platform", { get: () => "MacIntel" });
|
||||
});
|
||||
}
|
||||
|
||||
async function firstNavButtonX(page: import("@playwright/test").Page) {
|
||||
const toggle = page.locator('[data-testid="app-top-chrome"] button').first();
|
||||
await expect(toggle).toBeVisible();
|
||||
const box = await toggle.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
return box?.x ?? 0;
|
||||
}
|
||||
|
||||
// The chrome buttons are styled to visually match the fixed-size native
|
||||
// controls, so their box must not follow the rem text scale either. The
|
||||
// sidebar toggle is 28px square; the back/forward history buttons share the
|
||||
// height but are deliberately narrower (24px).
|
||||
const NAV_BUTTON_SIZE = 28;
|
||||
const HISTORY_BUTTON_WIDTH = 24;
|
||||
|
||||
// The grabber/drag strip hosting the buttons must hold its height too —
|
||||
// otherwise Cmd+ balloons the bar around the fixed-size buttons and Cmd-
|
||||
// collapses it.
|
||||
const TOP_CHROME_BAR_HEIGHT = 40;
|
||||
|
||||
async function expectTopChromeFixedHeight(
|
||||
page: import("@playwright/test").Page,
|
||||
) {
|
||||
const bar = page.getByTestId("app-top-chrome");
|
||||
await expect(bar).toBeVisible();
|
||||
const box = await bar.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
expect(box?.height ?? 0).toBe(TOP_CHROME_BAR_HEIGHT);
|
||||
}
|
||||
|
||||
async function expectNavButtonsFixedSize(
|
||||
page: import("@playwright/test").Page,
|
||||
) {
|
||||
const buttons = page.locator('[data-testid="app-top-chrome"] button');
|
||||
const count = await buttons.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const button = buttons.nth(i);
|
||||
const label = await button.getAttribute("aria-label");
|
||||
const isHistoryButton = label === "Go back" || label === "Go forward";
|
||||
const box = await button.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
expect(box?.width ?? 0).toBe(
|
||||
isHistoryButton ? HISTORY_BUTTON_WIDTH : NAV_BUTTON_SIZE,
|
||||
);
|
||||
expect(box?.height ?? 0).toBe(NAV_BUTTON_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
async function seedTextScale(
|
||||
page: import("@playwright/test").Page,
|
||||
scale: number,
|
||||
) {
|
||||
await page.addInitScript((value) => {
|
||||
window.localStorage.setItem("buzz:text-scale", String(value));
|
||||
}, scale);
|
||||
}
|
||||
|
||||
async function expectRootFontSize(
|
||||
page: import("@playwright/test").Page,
|
||||
fontSize: string,
|
||||
) {
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => getComputedStyle(document.documentElement).fontSize),
|
||||
)
|
||||
.toBe(fontSize);
|
||||
}
|
||||
|
||||
test.describe("top chrome macOS traffic-light clearance under text zoom", () => {
|
||||
test("nav buttons clear the traffic lights at default zoom", async ({
|
||||
page,
|
||||
}) => {
|
||||
await spoofMacPlatform(page);
|
||||
await installMockBridge(page);
|
||||
await page.goto("/");
|
||||
|
||||
expect(await firstNavButtonX(page)).toBeGreaterThanOrEqual(
|
||||
TRAFFIC_LIGHT_RIGHT_EDGE,
|
||||
);
|
||||
await expectNavButtonsFixedSize(page);
|
||||
await expectTopChromeFixedHeight(page);
|
||||
});
|
||||
|
||||
test("nav buttons still clear the traffic lights when zoomed out", async ({
|
||||
page,
|
||||
}) => {
|
||||
await spoofMacPlatform(page);
|
||||
// Seed the minimum Cmd- text scale (0.75). The old rem-based clearance
|
||||
// (pl-20 = 5rem) shrank to 60px here, sliding the buttons under the
|
||||
// fixed-position native controls.
|
||||
await seedTextScale(page, 0.75);
|
||||
await installMockBridge(page);
|
||||
await page.goto("/");
|
||||
|
||||
// Confirm the zoomed-out scale actually applied to the root font size.
|
||||
await expectRootFontSize(page, "12px");
|
||||
|
||||
expect(await firstNavButtonX(page)).toBeGreaterThanOrEqual(
|
||||
TRAFFIC_LIGHT_RIGHT_EDGE,
|
||||
);
|
||||
await expectNavButtonsFixedSize(page);
|
||||
await expectTopChromeFixedHeight(page);
|
||||
});
|
||||
|
||||
test("nav buttons keep their fixed size when zoomed in", async ({ page }) => {
|
||||
await spoofMacPlatform(page);
|
||||
// Seed the maximum Cmd+ text scale (1.5). Rem-sized buttons (h-7 = 42px
|
||||
// here) grew visibly taller than the fixed-size traffic lights.
|
||||
await seedTextScale(page, 1.5);
|
||||
await installMockBridge(page);
|
||||
await page.goto("/");
|
||||
|
||||
await expectRootFontSize(page, "24px");
|
||||
|
||||
expect(await firstNavButtonX(page)).toBeGreaterThanOrEqual(
|
||||
TRAFFIC_LIGHT_RIGHT_EDGE,
|
||||
);
|
||||
await expectNavButtonsFixedSize(page);
|
||||
await expectTopChromeFixedHeight(page);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user