{ setOpenSubmenuId(hasSub ? entry.id : null) }}
+ onMouseLeave={() => { setOpenSubmenuId(null) }}
+ >
+
{ setOpenSubmenuId(hasSub ? entry.id : null) }}
+ onClick={() => {
+ if (hasSub) {
+ setOpenSubmenuId(entry.id)
+ return
+ }
+ onSelect(entry.id)
+ }}
+ >
+ {entry.icon !== undefined && {entry.icon} }
+ {entry.label}
+ {/* Selection marker is a trailing check (figma .Menu_cell), not a fill. */}
+ {entry.id === selectedId && }
+
+ {subOpen && entry.submenu !== undefined && (
+
+ {entry.submenu.map(sub => (
+ { onSelect(sub.id) }}
+ >
+ {sub.icon !== undefined && {sub.icon} }
+ {sub.label}
+
+ ))}
+
+ )}
+
+ )
+ }
+
+ // Portal lists render hidden until placed: the placement effect measures
+ // this pre-render in the same commit, so the first painted frame is
+ // already at the final position (with getAnchorRect returning null the
+ // list simply stays hidden).
+ const list = open && (
{ onClose() } : undefined}
// React portals bubble synthetic events through the REACT tree: without
@@ -158,63 +255,14 @@ export function Menu({ open, anchor, items, selectedId, onSelect, onClose, align
// (open/toggle) after onSelect.
onClick={(e) => { e.stopPropagation() }}
>
- {items.map((entry) => {
- if (isSeparator(entry)) {
- return
- }
- if (isLabel(entry)) {
- return
{entry.text}
- }
- const hasSub = entry.submenu !== undefined && entry.submenu.length > 0
- const subOpen = hasSub && openSubmenuId === entry.id
- return (
-
{ setOpenSubmenuId(hasSub ? entry.id : null) }}
- onMouseLeave={() => { setOpenSubmenuId(null) }}
- >
-
{ setOpenSubmenuId(hasSub ? entry.id : null) }}
- onClick={() => {
- if (hasSub) {
- setOpenSubmenuId(entry.id)
- return
- }
- onSelect(entry.id)
- }}
- >
- {entry.icon !== undefined && {entry.icon} }
- {entry.label}
- {/* Selection marker is a trailing check (figma .Menu_cell), not a fill. */}
- {entry.id === selectedId && }
-
- {subOpen && entry.submenu !== undefined && (
-
- {entry.submenu.map(sub => (
- { onSelect(sub.id) }}
- >
- {sub.icon !== undefined && {sub.icon} }
- {sub.label}
-
- ))}
-
- )}
-
- )
- })}
+
+ {items.map(renderEntry)}
+
+ {footer !== undefined && footer.length > 0 && (
+
+ {footer.map(renderEntry)}
+
+ )}
)
diff --git a/packages/client/ui-primitives/src/Modal.module.css b/packages/client/ui-primitives/src/Modal.module.css
index 02c075803e..0c6d68e5fa 100644
--- a/packages/client/ui-primitives/src/Modal.module.css
+++ b/packages/client/ui-primitives/src/Modal.module.css
@@ -54,7 +54,7 @@
margin: 0;
font-size: 16px;
line-height: 24px;
- font-weight: 510;
+ font-weight: 500; /* figma wt510, rendered 500 */
color: var(--dsw-alias-label-primary);
}
diff --git a/packages/client/ui-primitives/src/Tooltip.tsx b/packages/client/ui-primitives/src/Tooltip.tsx
index 2c48854055..30e0c5fe0e 100644
--- a/packages/client/ui-primitives/src/Tooltip.tsx
+++ b/packages/client/ui-primitives/src/Tooltip.tsx
@@ -72,7 +72,7 @@ export function Tooltip({ label, side = 'right', disabled = false, children }: {
{cloneElement(children, {
ref: mergedRef,
onMouseEnter: (e) => { children.props.onMouseEnter?.(e); triggers.current.hover = true; show() },
- onMouseLeave: (e) => { children.props.onMouseLeave?.(e); triggers.current.hover = false; hide() },
+ onMouseLeave: (e) => { children.props.onMouseLeave?.(e); triggers.current.hover = false; setPos(null) },
onFocus: (e) => { children.props.onFocus?.(e); triggers.current.focus = true; show() },
onBlur: (e) => { children.props.onBlur?.(e); triggers.current.focus = false; hide() },
})}
diff --git a/packages/client/ui-primitives/tests/atoms.spec.tsx b/packages/client/ui-primitives/tests/atoms.spec.tsx
index 9e298d057a..7724afa493 100644
--- a/packages/client/ui-primitives/tests/atoms.spec.tsx
+++ b/packages/client/ui-primitives/tests/atoms.spec.tsx
@@ -271,14 +271,47 @@ describe('Menu', () => {
expect(onClose).toHaveBeenCalledTimes(1)
})
- it('portal mode positions from the opposite edges for align=end / side=top', () => {
+ it('portal mode resolves align=end / side=top to clamped left/top coordinates', () => {
render(