From ac70a9d759a5b0f8566664ef12ce96bdcf431cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 30 Sep 2026 03:58:26 -0400 Subject: [PATCH 1/4] feat(studio): a popover can point at its trigger with an optional arrow --- .../studio/src/components/ui/Menu.test.tsx | 20 +++++++++++++++++++ packages/studio/src/components/ui/Popover.tsx | 20 ++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/studio/src/components/ui/Menu.test.tsx b/packages/studio/src/components/ui/Menu.test.tsx index ef75f8b558..408fe83f37 100644 --- a/packages/studio/src/components/ui/Menu.test.tsx +++ b/packages/studio/src/components/ui/Menu.test.tsx @@ -298,6 +298,26 @@ describe("Popover", () => { expect(document.activeElement).toBe(input); expect(document.querySelector('[role="menu"]')).toBeNull(); }); + + it("points at its trigger only when asked", async () => { + const renderOpen = (arrow: boolean) => { + render( + Speed} arrow={arrow} defaultOpen> + 1x + , + ); + }; + renderOpen(false); + await settle(); + expect(document.querySelector("[role=dialog] svg")).toBeNull(); + act(() => mounted?.root.unmount()); + mounted = null; + + renderOpen(true); + await settle(); + const arrow = one("[role=dialog] svg", "arrow").parentElement!; + expect(arrow.getAttribute("data-side")).toBe("bottom"); + }); }); describe("hotkey classification", () => { diff --git a/packages/studio/src/components/ui/Popover.tsx b/packages/studio/src/components/ui/Popover.tsx index 43409e4e26..b4ec9c81fe 100644 --- a/packages/studio/src/components/ui/Popover.tsx +++ b/packages/studio/src/components/ui/Popover.tsx @@ -14,6 +14,14 @@ type PortalContainer = ComponentPropsWithoutRef["cont const SIDE_OFFSET = 6; const VIEWPORT_MARGIN = 8; +/** The arrow's 14x8 box overlaps the popup's border by 1px, so its fill hides the border where it meets the popup. */ +const ARROW_PX = 7; +const arrowPlace = cn( + "data-[side=bottom]:-top-[7px] data-[side=top]:-bottom-[7px] data-[side=top]:rotate-180", + "data-[side=left]:-right-[10px] data-[side=left]:rotate-90", + "data-[side=right]:-left-[10px] data-[side=right]:-rotate-90", +); + interface PopoverProps extends Omit, "children"> { /** A single element. It becomes the trigger; no wrapper is added around it. */ trigger: ReactElement; @@ -22,6 +30,8 @@ interface PopoverProps extends Omit + {arrow && ( + + + + )} {children} From b1f5a5cff03748e597fd7235a93bf449067ae885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 30 Sep 2026 04:21:04 -0400 Subject: [PATCH 2/4] fix(studio): keep a side arrow off the popover corner --- packages/studio/src/components/ui/Menu.test.tsx | 3 ++- packages/studio/src/components/ui/Popover.tsx | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/studio/src/components/ui/Menu.test.tsx b/packages/studio/src/components/ui/Menu.test.tsx index 408fe83f37..7c042264d7 100644 --- a/packages/studio/src/components/ui/Menu.test.tsx +++ b/packages/studio/src/components/ui/Menu.test.tsx @@ -309,8 +309,9 @@ describe("Popover", () => { }; renderOpen(false); await settle(); - expect(document.querySelector("[role=dialog] svg")).toBeNull(); + expect(one("[role=dialog]", "popup").querySelector("svg")).toBeNull(); act(() => mounted?.root.unmount()); + mounted?.host.remove(); mounted = null; renderOpen(true); diff --git a/packages/studio/src/components/ui/Popover.tsx b/packages/studio/src/components/ui/Popover.tsx index b4ec9c81fe..9b5082796c 100644 --- a/packages/studio/src/components/ui/Popover.tsx +++ b/packages/studio/src/components/ui/Popover.tsx @@ -16,6 +16,8 @@ const VIEWPORT_MARGIN = 8; /** The arrow's 14x8 box overlaps the popup's border by 1px, so its fill hides the border where it meets the popup. */ const ARROW_PX = 7; +// A left or right arrow is rotated, 14px along the edge, so it needs this room to clear the rounded corner. +const ARROW_PADDING = 10; const arrowPlace = cn( "data-[side=bottom]:-top-[7px] data-[side=top]:-bottom-[7px] data-[side=top]:rotate-180", "data-[side=left]:-right-[10px] data-[side=left]:rotate-90", @@ -30,7 +32,7 @@ interface PopoverProps extends Omit Date: Wed, 30 Sep 2026 04:41:13 -0400 Subject: [PATCH 3/4] refactor(studio): name the side arrow corner clearance instead of commenting it --- packages/studio/src/components/ui/Popover.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/studio/src/components/ui/Popover.tsx b/packages/studio/src/components/ui/Popover.tsx index 9b5082796c..057e25df3f 100644 --- a/packages/studio/src/components/ui/Popover.tsx +++ b/packages/studio/src/components/ui/Popover.tsx @@ -16,8 +16,7 @@ const VIEWPORT_MARGIN = 8; /** The arrow's 14x8 box overlaps the popup's border by 1px, so its fill hides the border where it meets the popup. */ const ARROW_PX = 7; -// A left or right arrow is rotated, 14px along the edge, so it needs this room to clear the rounded corner. -const ARROW_PADDING = 10; +const ROTATED_ARROW_CORNER_CLEARANCE = 10; const arrowPlace = cn( "data-[side=bottom]:-top-[7px] data-[side=top]:-bottom-[7px] data-[side=top]:rotate-180", "data-[side=left]:-right-[10px] data-[side=left]:rotate-90", @@ -70,7 +69,7 @@ export function Popover({ align={align} sideOffset={sideOffset} collisionPadding={VIEWPORT_MARGIN} - arrowPadding={ARROW_PADDING} + arrowPadding={ROTATED_ARROW_CORNER_CLEARANCE} className="z-200" > Date: Wed, 30 Sep 2026 04:52:24 -0400 Subject: [PATCH 4/4] refactor(studio): the arrow corner clearance applies on every side --- packages/studio/src/components/ui/Popover.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/studio/src/components/ui/Popover.tsx b/packages/studio/src/components/ui/Popover.tsx index 057e25df3f..3a070883ca 100644 --- a/packages/studio/src/components/ui/Popover.tsx +++ b/packages/studio/src/components/ui/Popover.tsx @@ -16,7 +16,7 @@ const VIEWPORT_MARGIN = 8; /** The arrow's 14x8 box overlaps the popup's border by 1px, so its fill hides the border where it meets the popup. */ const ARROW_PX = 7; -const ROTATED_ARROW_CORNER_CLEARANCE = 10; +const ARROW_CORNER_CLEARANCE = 10; const arrowPlace = cn( "data-[side=bottom]:-top-[7px] data-[side=top]:-bottom-[7px] data-[side=top]:rotate-180", "data-[side=left]:-right-[10px] data-[side=left]:rotate-90", @@ -69,7 +69,7 @@ export function Popover({ align={align} sideOffset={sideOffset} collisionPadding={VIEWPORT_MARGIN} - arrowPadding={ROTATED_ARROW_CORNER_CLEARANCE} + arrowPadding={ARROW_CORNER_CLEARANCE} className="z-200" >