减重:全部右键菜单(FileTree 行菜单 / TabBar tab 菜单 / GitLens 文件与 历史菜单 / FreeWindow 标题菜单)与「+」菜单切到宿主 Menu 原语的 compact 密度(26px 行高 / 12px 字 / 164px 卡片),图标随槽位 16→14,「打开方式」 子菜单图钉 20→16,还原工具型右键菜单的趁手感。 越界:宿主子菜单纯 CSS 定位(从父行向上、向右生长)不受视口钳制——树顶 部右键时「打开方式 ▸」子菜单伸出视口上沿,窄面板(280–400px)时伸出右 沿。新增 menu-flip.ts:菜单打开期间在 <body> 发布 data-dsh-sidebar-submenu ="down|left" 方向 token(y < vh/2 → down;x + 400 > vw → left),layout.css 据此翻转子菜单生长方向与 ::before 悬停桥;属性存续期即作用域,宿主自有 菜单不受影响。刻意不做 max-height 滚动钳制——overflow 裁剪会切断悬停桥。 测试:menu-flip.spec.tsx(token 纯函数 + hook 生命周期 + CSS 契约跨文件 一致性);tab-bar / file-tree 两个 spec 追加翻转向量断言;mount.e2e 追加 compact 行高(≤30px)布局级证据。设计偏差记录同步至 docs/plans/2026-08-22-open-with-menu-design.md。
69 lines
3.1 KiB
TypeScript
69 lines
3.1 KiB
TypeScript
/**
|
|
* Submenu flip geometry for the portaled context menus.
|
|
*
|
|
* The primitives `Menu` clamps its main list into the viewport (portal mode,
|
|
* 12px margin) but positions nested submenus with pure CSS — `bottom: -4px`
|
|
* (grow UP from the parent row) and `left: calc(100% + 10px)` (open RIGHT of
|
|
* the card) — with no viewport clamp at all. This sidebar lives docked on the
|
|
* RIGHT edge of the window, so both defaults are the worst case here: a tall
|
|
* "open with" submenu pokes past the viewport top when the row menu opens
|
|
* near the top, and past the right edge whenever the panel is narrow.
|
|
*
|
|
* The host primitive is not ours to patch, so the fix is a body attribute
|
|
* written while one of OUR submenu-bearing menus is open: layout.css flips
|
|
* the submenu's growth direction per token. The attribute's lifetime IS the
|
|
* scope — only our menus set it, and layout.css rules for
|
|
* `div[role="menu"] div[role="menu"]` only apply while it exists, so
|
|
* host-owned menus are never touched.
|
|
*/
|
|
|
|
import { useEffect } from 'react'
|
|
|
|
/** body attribute carrying the flip tokens (whitespace-separated words). */
|
|
export const SUBMENU_FLIP_ATTR = 'data-dsh-sidebar-submenu'
|
|
|
|
/**
|
|
* Mirror of the primitive's submenu geometry (Menu.module.css): card
|
|
* min-width 218 + card-to-submenu gap 10 + submenu min-width 163..165 +
|
|
* viewport margin 12 ≈ 400px of right-hand room a right-growing submenu
|
|
* needs. If the cursor is within that of the right edge, open left instead.
|
|
* The estimate is conservative (the primitive's own clamp can only move the
|
|
* card further left), so a wrong guess flips left unnecessarily — never into
|
|
* a new overflow — and drifting from future host metrics degrades the same
|
|
* way, back to today's behavior at worst.
|
|
*/
|
|
const SUBMENU_RIGHT_ROOM = 400
|
|
|
|
/** Below the vertical midpoint the parent row usually has more room above. */
|
|
function isUpperHalf(y: number, vh: number): boolean {
|
|
return y < vh / 2
|
|
}
|
|
|
|
/**
|
|
* The flip tokens for a menu opened at viewport coordinates (x, y):
|
|
* `down` — open the submenu downward from the parent row (cursor in the
|
|
* upper half; the sidebar's tab strip is always up there);
|
|
* `left` — open the submenu leftward of the card (not enough right-hand
|
|
* room). Empty string when the primitive's defaults already fit.
|
|
*/
|
|
export function submenuFlipTokens(x: number, y: number, vw: number, vh: number): string {
|
|
const tokens: string[] = []
|
|
if (isUpperHalf(y, vh)) tokens.push('down')
|
|
if (x + SUBMENU_RIGHT_ROOM > vw) tokens.push('left')
|
|
return tokens.join(' ')
|
|
}
|
|
|
|
/**
|
|
* Publish the flip tokens on <body> while the given cursor-anchored menu
|
|
* state is open; clear them on close and on unmount (a menu left open by an
|
|
* unmounting panel must not leave stale geometry on the shared body).
|
|
* `menu` only needs the viewport coordinates the Menu anchored to.
|
|
*/
|
|
export function useSubmenuFlip(menu: { x: number; y: number } | null): void {
|
|
useEffect(() => {
|
|
if (menu === null) return
|
|
document.body.setAttribute(SUBMENU_FLIP_ATTR, submenuFlipTokens(menu.x, menu.y, window.innerWidth, window.innerHeight))
|
|
return () => { document.body.removeAttribute(SUBMENU_FLIP_ATTR) }
|
|
}, [menu])
|
|
}
|