镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions apps/web/src/components/preview/PreviewPanelShell.browser.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { page, userEvent } from "vite-plus/test/browser";
import { describe, expect, it, vi } from "vite-plus/test";

import { render } from "../../../test/render";
import { PreviewPanelShell } from "./PreviewPanelShell";

// The handle only has a hit area once the app stylesheet positions it.
import "~/index.css";

const WIDTH_STORAGE_KEY = "t3code:test:preview-panel-width";

function Row({ width }: { width: number }) {
return (
<div style={{ display: "flex", width, height: 400 }}>
<div style={{ flex: 1, minWidth: 0 }} />
<PreviewPanelShell mode="inline" open widthStorageKey={WIDTH_STORAGE_KEY} defaultWidth={400}>
<div />
</PreviewPanelShell>
</div>
);
}

describe("PreviewPanelShell", () => {
it.each([
{ rowWidth: 1200, expected: 600, outcome: "half of its row" },
{ rowWidth: 600, expected: 360, outcome: "the minimum width when half the row is narrower" },
])(
"double-clicking the resize handle gives the panel $outcome",
async ({ rowWidth, expected }) => {
await page.viewport(1400, 800);
window.localStorage.removeItem(WIDTH_STORAGE_KEY);
using _storage = {
[Symbol.dispose]: () => window.localStorage.removeItem(WIDTH_STORAGE_KEY),
};
await using screen = await render(<Row width={rowWidth} />, {
router: false /* no routes involved */,
});
const panel = screen.container.querySelector<HTMLElement>(
'[data-preview-panel-mode="inline"]',
)!;
expect(window.localStorage.getItem(WIDTH_STORAGE_KEY)).toBeNull();

await userEvent.dblClick(screen.getByRole("separator"));

await vi.waitFor(() => expect(panel.getBoundingClientRect().width).toBe(expected));
expect(window.localStorage.getItem(WIDTH_STORAGE_KEY)).toBe(String(expected));
},
);
});
17 changes: 14 additions & 3 deletions apps/web/src/hooks/usePreviewPanelInlineSize.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { type MouseEvent as ReactMouseEvent, useState } from "react";

import { type ResizableWidthHandlers, useResizableWidth } from "./useResizableWidth";
import { observeResize } from "../lib/observeResize";
Expand Down Expand Up @@ -30,15 +30,26 @@ export function usePreviewPanelInlineSize(
const [maxWidth, setMaxWidth] = useState(() =>
getPreviewPanelMaxWidth(typeof window === "undefined" ? 1280 : window.innerWidth),
);
const { width, handlers } = useResizableWidth({
const {
width,
handlers: dragHandlers,
setWidth,
} = useResizableWidth({
storageKey: options.widthStorageKey ?? PREVIEW_PANEL_WIDTH_STORAGE_KEY,
defaultWidth: options.defaultWidth ?? PREVIEW_PANEL_DEFAULT_WIDTH,
minWidth: PREVIEW_PANEL_MIN_WIDTH,
maxWidth,
edge: "left",
});
const resolveRow = (host: HTMLElement | null) =>
container ?? (enabled ? host?.parentElement : null) ?? null;
const centerInRow = (event: ReactMouseEvent<HTMLElement>) => {
const row = resolveRow(event.currentTarget.parentElement);
if (row) setWidth(Math.round(row.clientWidth / 2));
};
const handlers: ResizableWidthHandlers = { ...dragHandlers, onDoubleClick: centerInRow };
const observeRow = (host: HTMLElement) => {
const row = container ?? (enabled ? host.parentElement : null) ?? null;
const row = resolveRow(host);
// Ref callbacks run before paint, so the width is clamped without an over-wide
// flash on mount. Only the derived cap is stored: the row resizes every frame
// of a sidebar drag, but an unchanged cap skips re-rendering the owner.
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/hooks/useResizableWidth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as Schema from "effect/Schema";
import { type PointerEvent as ReactPointerEvent, useLayoutEffect, useState } from "react";
import {
type MouseEvent as ReactMouseEvent,
type PointerEvent as ReactPointerEvent,
useLayoutEffect,
useState,
} from "react";

import { getLocalStorageItem, setLocalStorageItem } from "./useLocalStorage";
import { useResizeDrag } from "./useResizeDrag";
Expand All @@ -24,6 +29,7 @@ export interface ResizableWidthHandlers {
readonly onPointerUp: (event: ReactPointerEvent<HTMLElement>) => void;
readonly onPointerCancel: (event: ReactPointerEvent<HTMLElement>) => void;
readonly onLostPointerCapture: (event: ReactPointerEvent<HTMLElement>) => void;
readonly onDoubleClick?: (event: ReactMouseEvent<HTMLElement>) => void;
}

/** Persist on drag end to avoid a storage write on every animation frame. */
Expand Down
Loading