diff --git a/src/styles/tokens.css b/src/styles/tokens.css index a572274a..d9eb0eb9 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -1159,8 +1159,8 @@ html { --palette-fg: #657b83; --palette-muted: #657b83; --palette-border: #eee8d5; - --palette-hover: #eee8d5; - --palette-selection: #eee8d5; + --palette-hover: #e4ddc8; + --palette-selection: #c2d3d4; --palette-line: #eee8d5; --palette-accent: #268bd2; --palette-on-accent: #002b36; @@ -1191,6 +1191,9 @@ html { --syn-markup-code: #2aa198; --syn-tag: #268bd2; --syn-escape: #dc322f; + /* Selection and hover differ from the panel and popup surfaces; the editor keeps Solarized's own selection. */ + --bg-editor-selection: #eee8d5; + --bg-editor-selection-inactive: #eee8d5; } :root[data-color-theme="solarized-dark"] { @@ -1208,8 +1211,8 @@ html { --palette-fg: #839496; --palette-muted: #839496; --palette-border: #073642; - --palette-hover: #073642; - --palette-selection: #073642; + --palette-hover: #0b4053; + --palette-selection: #10506d; --palette-line: #073642; --palette-accent: #268bd2; --palette-on-accent: #002b36; @@ -1240,6 +1243,9 @@ html { --syn-markup-code: #2aa198; --syn-tag: #268bd2; --syn-escape: #dc322f; + /* Selection and hover differ from the panel and popup surfaces; the editor keeps Solarized's own selection. */ + --bg-editor-selection: #073642; + --bg-editor-selection-inactive: #073642; } :root[data-color-theme="one-light"] { diff --git a/src/ui/Menu.tsx b/src/ui/Menu.tsx index 74ef0dcc..0b083979 100644 --- a/src/ui/Menu.tsx +++ b/src/ui/Menu.tsx @@ -125,13 +125,22 @@ export function MenuList({ items, onDone, onCloseLevel, autoFocusFirst = true, . const [subAnchor, setSubAnchor] = useState(null); const itemRefs = useRef>([]); const hoverTimer = useRef | undefined>(undefined); + // Pointer movement highlights a row without focusing it. Focusing on every + // enter drops the hover paint in the Windows webview, so the menu looks frozen. + const pointerHover = useRef(false); + // Row the pointer last entered: pointerenter and mouseenter both report it. + const hovered = useRef(-1); const setOpenSub = (i: number | null) => { setSubAnchor(i === null ? null : (itemRefs.current[i] ?? null)); setOpenSubIndex(i); }; useLayoutEffect(() => { - if (active >= 0 && openSub === null) itemRefs.current[active]?.focus({ preventScroll: false }); + if (pointerHover.current) { + pointerHover.current = false; + return; + } + if (active >= 0 && openSub === null) itemRefs.current[active]?.focus({ preventScroll: true }); }, [active, openSub]); useEffect(() => () => clearTimeout(hoverTimer.current), []); @@ -165,6 +174,20 @@ export function MenuList({ items, onDone, onCloseLevel, autoFocusFirst = true, . item!.run?.(); }; + const hover = (i: number) => { + const item = resolved[i]; + if (!item || item.disabled || item.kind !== "item") return; + if (hovered.current !== i) { + hovered.current = i; + if (i !== active) { + pointerHover.current = true; + focusItem(i); + } + } + clearTimeout(hoverTimer.current); + hoverTimer.current = setTimeout(() => setOpenSub(item.submenu ? i : null), item.submenu ? 120 : 200); + }; + const onKeyDown = (e: ReactKeyboardEvent) => { switch (e.key) { case "ArrowDown": @@ -241,12 +264,10 @@ export function MenuList({ items, onDone, onCloseLevel, autoFocusFirst = true, . r.disabled && styles.disabled, r.danger && styles.danger, )} - onPointerEnter={() => { - if (r.disabled) return; - focusItem(i); - clearTimeout(hoverTimer.current); - hoverTimer.current = setTimeout(() => setOpenSub(hasSub ? i : null), hasSub ? 120 : 200); - }} + onPointerEnter={() => hover(i)} + onMouseEnter={() => hover(i)} + onPointerLeave={() => (hovered.current = -1)} + onMouseLeave={() => (hovered.current = -1)} onClick={() => activate(i)} > diff --git a/src/ui/Popup.test.tsx b/src/ui/Popup.test.tsx index ed98b039..2d0c2492 100644 --- a/src/ui/Popup.test.tsx +++ b/src/ui/Popup.test.tsx @@ -5,6 +5,7 @@ import { getContext, setContext } from "@/platform/context"; import { registerAction } from "@/platform/actions"; import { Popup } from "./Popup"; import { ContextMenuHost, Menu, MenuList, hideContextMenu, showContextMenu } from "./Menu"; +import menuStyles from "./Menu.module.css"; import { Dialog } from "./Dialog"; function Harness({ onClose }: { onClose?: (r: string) => void }) { @@ -192,6 +193,41 @@ describe("Menu action enablement", () => { expect(input).toHaveFocus(); }); + it("highlights the row under the pointer without moving focus", () => { + render( + undefined }, + { label: "Save", run: () => undefined }, + ]} + onDone={() => {}} + />, + ); + const open = screen.getByRole("menuitem", { name: "Open" }); + const save = screen.getByRole("menuitem", { name: "Save" }); + expect(open).toHaveFocus(); + expect(open.className).toContain(menuStyles.active); + fireEvent.pointerEnter(save); + expect(save.className).toContain(menuStyles.active); + expect(open.className).not.toContain(menuStyles.active); + expect(open).toHaveFocus(); + }); + + it("reports a hovered row once and lets the keyboard continue from it", () => { + const active: string[] = []; + const item = (label: string) => ({ label, onActive: () => active.push(label), run: () => undefined }); + render( {}} />); + const save = screen.getByRole("menuitem", { name: "Save" }); + fireEvent.pointerEnter(save); + fireEvent.mouseEnter(save); + expect(active).toEqual(["Save"]); + fireEvent.keyDown(save, { key: "ArrowDown" }); + expect(screen.getByRole("menuitem", { name: "Close" })).toHaveFocus(); + fireEvent.pointerLeave(save); + fireEvent.pointerEnter(save); + expect(active).toEqual(["Save", "Close", "Save"]); + }); + it("follows context changes after the menu opened", () => { const dispose = registerAction({ id: "test.menuWhen", title: "Needs Flag", when: "qaMenuFlag", run: () => {} }); setContext("qaMenuFlag", false); diff --git a/src/workbench/MainMenu.tsx b/src/workbench/MainMenu.tsx index f9adf94a..53dc658e 100644 --- a/src/workbench/MainMenu.tsx +++ b/src/workbench/MainMenu.tsx @@ -229,6 +229,9 @@ function MainMenuBar({ merged, onExpandedChange }: MainMenuProps & { merged: boo onPointerEnter={() => { if (open !== null) setOpen(i); }} + onMouseEnter={() => { + if (open !== null) setOpen(i); + }} onClick={() => { if (open === i) close(); else setOpen(i);