diff --git a/apps/web/src/browser/BrowserDeviceToolbar.browser.test.tsx b/apps/web/src/browser/BrowserDeviceToolbar.browser.test.tsx new file mode 100644 index 0000000000000..65b58b6c4f683 --- /dev/null +++ b/apps/web/src/browser/BrowserDeviceToolbar.browser.test.tsx @@ -0,0 +1,71 @@ +import type { PreviewViewportSetting } from "@t3tools/contracts"; +import { useState } from "react"; +import { expect, it } from "vite-plus/test"; +import { userEvent } from "vite-plus/test/browser"; + +import { render } from "../../test/render"; +import { BrowserDeviceToolbar } from "./BrowserDeviceToolbar"; + +function Viewport({ width = 1920, height = 1080 }: { width?: number; height?: number }) { + const [setting, setSetting] = useState({ + _tag: "freeform", + width, + height, + }); + const [aspectRatio, setAspectRatio] = useState(width / height); + if (setting._tag === "fill") throw new Error("Expected fixed viewport"); + return ( + <> + setSetting(next)} + /> + + {setting.width} × {setting.height} + + + + ); +} + +it.each([ + { width: 1920, height: 1080, axis: "width", typed: "2560", expected: "2560 × 1440" }, + { width: 390, height: 844, axis: "height", typed: "2532", expected: "1170 × 2532" }, +])("keeps each typed $axis digit until the locked size is committed", async (testCase) => { + await using screen = await render(); + const input = screen.getByRole("spinbutton", { name: `Viewport ${testCase.axis}` }); + await userEvent.clear(input); + await userEvent.type(input, testCase.typed); + await expect.element(input).toHaveValue(Number(testCase.typed)); + await screen.getByRole("button", { name: "Leave toolbar" }).click(); + await expect + .element(screen.getByRole("status", { name: "Saved viewport" })) + .toHaveTextContent(testCase.expected); +}); + +it("clamps a completed width below the locked minimum when leaving the toolbar", async () => { + await using screen = await render(); + const input = screen.getByRole("spinbutton", { name: "Viewport width" }); + await userEvent.clear(input); + await userEvent.type(input, "256"); + await expect.element(input).toHaveValue(256); + await screen.getByRole("button", { name: "Leave toolbar" }).click(); + await expect + .element(screen.getByRole("status", { name: "Saved viewport" })) + .toHaveTextContent("427 × 240"); +}); + +it("still limits a completed locked size to the viewport area cap", async () => { + await using screen = await render(); + const input = screen.getByRole("spinbutton", { name: "Viewport width" }); + await userEvent.clear(input); + await userEvent.type(input, "3840"); + await expect.element(input).toHaveValue(3840); + await screen.getByRole("button", { name: "Leave toolbar" }).click(); + await expect + .element(screen.getByRole("status", { name: "Saved viewport" })) + .toHaveTextContent("2880 × 2880"); +}); diff --git a/apps/web/src/browser/BrowserDeviceToolbar.tsx b/apps/web/src/browser/BrowserDeviceToolbar.tsx index fbd843c8d43fd..a880b3e42efe8 100644 --- a/apps/web/src/browser/BrowserDeviceToolbar.tsx +++ b/apps/web/src/browser/BrowserDeviceToolbar.tsx @@ -51,6 +51,7 @@ export function BrowserDeviceToolbar({ const [customSize, setCustomSize] = useState<{ readonly width: string; readonly height: string; + readonly axis?: "width" | "height"; } | null>(null); const presentedSize = customSize ?? { width: String(setting.width), @@ -61,15 +62,25 @@ export function BrowserDeviceToolbar({ PREVIEW_VIEWPORT_PRESETS.some((preset) => preset.id === setting.presetId) ? setting.presetId : RESPONSIVE_VALUE; - const customWidth = Number(presentedSize.width); - const customHeight = Number(presentedSize.height); + const typedWidth = Number(presentedSize.width); + const typedHeight = Number(presentedSize.height); + const { width: customWidth, height: customHeight } = + aspectRatio === null || customSize?.axis === undefined + ? { width: typedWidth, height: typedHeight } + : resizeFreeformViewport( + { width: typedWidth, height: typedHeight }, + { x: 0, y: 0 }, + 1, + customSize?.axis === "height" ? "south" : "east", + aspectRatio, + ); const customValid = - Number.isInteger(customWidth) && - Number.isInteger(customHeight) && - customWidth >= PREVIEW_VIEWPORT_MIN_DIMENSION && - customWidth <= PREVIEW_VIEWPORT_MAX_DIMENSION && - customHeight >= PREVIEW_VIEWPORT_MIN_DIMENSION && - customHeight <= PREVIEW_VIEWPORT_MAX_DIMENSION && + Number.isInteger(typedWidth) && + Number.isInteger(typedHeight) && + typedWidth >= PREVIEW_VIEWPORT_MIN_DIMENSION && + typedWidth <= PREVIEW_VIEWPORT_MAX_DIMENSION && + typedHeight >= PREVIEW_VIEWPORT_MIN_DIMENSION && + typedHeight <= PREVIEW_VIEWPORT_MAX_DIMENSION && customWidth * customHeight <= PREVIEW_VIEWPORT_MAX_AREA; const apply = (next: PreviewViewportSetting, nextAspectRatio = aspectRatio) => { @@ -96,6 +107,7 @@ export function BrowserDeviceToolbar({ const next = { width: axis === "width" ? value : (current?.width ?? String(setting.width)), height: axis === "height" ? value : (current?.height ?? String(setting.height)), + axis, }; const numeric = Number(value); if ( @@ -115,7 +127,11 @@ export function BrowserDeviceToolbar({ axis === "width" ? "east" : "south", aspectRatio, ); - return { width: String(resized.width), height: String(resized.height) }; + return { + ...next, + width: axis === "width" ? value : String(resized.width), + height: axis === "height" ? value : String(resized.height), + }; }); };