@@ -40,6 +40,48 @@ const ANIMATION_CLASSES =
4040const MENU_ROW_HEIGHT_CLASS = 'h-[28px]'
4141const MENU_ROW_RADIUS_CLASS = 'rounded-lg'
4242
43+ /**
44+ * Rows are a fixed height, so a label that wraps overflows its row and paints
45+ * over its neighbours instead of growing the row. Every row is therefore held
46+ * to one line, and its label ellipsizes — see {@link withEllipsizedLabel}.
47+ */
48+ const MENU_ROW_SINGLE_LINE_CLASS = 'whitespace-nowrap [&>span]:min-w-0 [&>span]:truncate'
49+
50+ /**
51+ * Wraps a row's bare text children in a truncating box so a label wider than
52+ * the menu ends in an ellipsis rather than being cut mid-word at the surface
53+ * edge. Consumers that already wrap their label in a `<span>` are unaffected —
54+ * the row's `[&>span]` rule truncates those in place.
55+ *
56+ * Adjacent text is coalesced into a single box: a row is a flex container, so
57+ * wrapping `Insert row {n}` as two boxes would make them two flex items and
58+ * open the row's `gap` between the words. `React.Children.toArray` keys the
59+ * element children it returns, so the rebuilt array needs no keys of its own.
60+ */
61+ function withEllipsizedLabel ( children : React . ReactNode ) : React . ReactNode {
62+ const rebuilt : React . ReactNode [ ] = [ ]
63+ let text : React . ReactNode [ ] = [ ]
64+ const flushText = ( ) => {
65+ if ( text . length === 0 ) return
66+ rebuilt . push (
67+ < span key = { `label-${ rebuilt . length } ` } className = 'min-w-0 truncate' >
68+ { text }
69+ </ span >
70+ )
71+ text = [ ]
72+ }
73+ for ( const child of React . Children . toArray ( children ) ) {
74+ if ( typeof child === 'string' || typeof child === 'number' ) {
75+ text . push ( child )
76+ continue
77+ }
78+ flushText ( )
79+ rebuilt . push ( child )
80+ }
81+ flushText ( )
82+ return rebuilt
83+ }
84+
4385/**
4486 * Surface corner, shared by the root menu and submenus — they previously
4587 * disagreed, at 12px and 8px.
@@ -110,13 +152,13 @@ const DropdownMenuSubTrigger = React.forwardRef<
110152 < DropdownMenuPrimitive . SubTrigger
111153 ref = { ref }
112154 className = { cn (
113- `flex ${ MENU_ROW_HEIGHT_CLASS } min-w-0 cursor-default select-none items-center gap-2 ${ MENU_ROW_RADIUS_CLASS } px-2 text-[var(--text-body)] text-small outline-none transition-colors focus:bg-[var(--surface-active)] data-[state=open]:bg-[var(--surface-active)] [&>span]:min-w-0 [&>span]:truncate [&_svg]:pointer-events-none [&_svg]:size-[14px] [&_svg]:shrink-0 [&_svg]:text-[var(--text-icon)]` ,
155+ `flex ${ MENU_ROW_HEIGHT_CLASS } min-w-0 cursor-default select-none items-center gap-2 ${ MENU_ROW_RADIUS_CLASS } px-2 text-[var(--text-body)] text-small outline-none transition-colors focus:bg-[var(--surface-active)] data-[state=open]:bg-[var(--surface-active)] ${ MENU_ROW_SINGLE_LINE_CLASS } [&_svg]:pointer-events-none [&_svg]:size-[14px] [&_svg]:shrink-0 [&_svg]:text-[var(--text-icon)]` ,
114156 inset && 'pl-7' ,
115157 className
116158 ) }
117159 { ...props }
118160 >
119- { children }
161+ { withEllipsizedLabel ( children ) }
120162 < ChevronRight className = 'ml-auto size-[14px] shrink-0' />
121163 </ DropdownMenuPrimitive . SubTrigger >
122164 )
@@ -172,7 +214,7 @@ const DropdownMenuContent = React.forwardRef<
172214) )
173215DropdownMenuContent . displayName = DropdownMenuPrimitive . Content . displayName
174216
175- const DROPDOWN_MENU_ITEM_BASE_CLASSES = `relative flex ${ MENU_ROW_HEIGHT_CLASS } min-w-0 cursor-pointer select-none items-center gap-2 ${ MENU_ROW_RADIUS_CLASS } px-2 text-[var(--text-body)] text-small outline-none transition-colors focus:bg-[var(--surface-active)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>span]:min-w-0 [&>span]:truncate [&_svg]:pointer-events-none [&_svg]:size-[14px] [&_svg]:shrink-0 [&_svg]:text-[var(--text-icon)]`
217+ const DROPDOWN_MENU_ITEM_BASE_CLASSES = `relative flex ${ MENU_ROW_HEIGHT_CLASS } min-w-0 cursor-pointer select-none items-center gap-2 ${ MENU_ROW_RADIUS_CLASS } px-2 text-[var(--text-body)] text-small outline-none transition-colors focus:bg-[var(--surface-active)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50 ${ MENU_ROW_SINGLE_LINE_CLASS } [&_svg]:pointer-events-none [&_svg]:size-[14px] [&_svg]:shrink-0 [&_svg]:text-[var(--text-icon)]`
176218
177219const DropdownMenuItem = React . forwardRef <
178220 React . ElementRef < typeof DropdownMenuPrimitive . Item > ,
@@ -185,7 +227,8 @@ const DropdownMenuItem = React.forwardRef<
185227 */
186228 action ?: React . ReactNode
187229 }
188- > ( ( { className, inset, action, ...props } , ref ) => {
230+ > ( ( { className, inset, action, asChild, children, ...props } , ref ) => {
231+ const content = asChild ? children : withEllipsizedLabel ( children )
189232 if ( action ) {
190233 return (
191234 < div className = 'group/dropdownitem relative' >
@@ -197,8 +240,11 @@ const DropdownMenuItem = React.forwardRef<
197240 inset && 'pl-7' ,
198241 className
199242 ) }
243+ asChild = { asChild }
200244 { ...props }
201- />
245+ >
246+ { content }
247+ </ DropdownMenuPrimitive . Item >
202248 < div className = '-translate-y-1/2 absolute top-1/2 right-1 flex items-center opacity-0 transition-opacity group-focus-within/dropdownitem:opacity-100 group-hover/dropdownitem:opacity-100' >
203249 { action }
204250 </ div >
@@ -209,8 +255,11 @@ const DropdownMenuItem = React.forwardRef<
209255 < DropdownMenuPrimitive . Item
210256 ref = { ref }
211257 className = { cn ( DROPDOWN_MENU_ITEM_BASE_CLASSES , inset && 'pl-7' , className ) }
258+ asChild = { asChild }
212259 { ...props }
213- />
260+ >
261+ { content }
262+ </ DropdownMenuPrimitive . Item >
214263 )
215264} )
216265DropdownMenuItem . displayName = DropdownMenuPrimitive . Item . displayName
@@ -252,7 +301,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
252301 < DropdownMenuPrimitive . CheckboxItem
253302 ref = { ref }
254303 className = { cn (
255- `relative flex ${ MENU_ROW_HEIGHT_CLASS } cursor-default select-none items-center ${ MENU_ROW_RADIUS_CLASS } pr-2 pl-7 text-[var(--text-body)] text-small outline-none transition-colors focus:bg-[var(--surface-active)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50` ,
304+ `relative flex ${ MENU_ROW_HEIGHT_CLASS } min-w-0 cursor-default select-none items-center ${ MENU_ROW_RADIUS_CLASS } whitespace-nowrap pr-2 pl-7 text-[var(--text-body)] text-small outline-none transition-colors focus:bg-[var(--surface-active)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50` ,
256305 className
257306 ) }
258307 checked = { checked }
@@ -263,7 +312,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
263312 < Check className = 'size-[14px]' />
264313 </ DropdownMenuPrimitive . ItemIndicator >
265314 </ span >
266- { children }
315+ { withEllipsizedLabel ( children ) }
267316 </ DropdownMenuPrimitive . CheckboxItem >
268317) )
269318DropdownMenuCheckboxItem . displayName = DropdownMenuPrimitive . CheckboxItem . displayName
@@ -275,7 +324,7 @@ const DropdownMenuRadioItem = React.forwardRef<
275324 < DropdownMenuPrimitive . RadioItem
276325 ref = { ref }
277326 className = { cn (
278- `relative flex ${ MENU_ROW_HEIGHT_CLASS } cursor-default select-none items-center ${ MENU_ROW_RADIUS_CLASS } pr-2 pl-7 text-[var(--text-body)] text-small outline-none transition-colors focus:bg-[var(--surface-active)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50` ,
327+ `relative flex ${ MENU_ROW_HEIGHT_CLASS } min-w-0 cursor-default select-none items-center ${ MENU_ROW_RADIUS_CLASS } whitespace-nowrap pr-2 pl-7 text-[var(--text-body)] text-small outline-none transition-colors focus:bg-[var(--surface-active)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50` ,
279328 className
280329 ) }
281330 { ...props }
@@ -285,7 +334,7 @@ const DropdownMenuRadioItem = React.forwardRef<
285334 < Circle className = 'size-[6px] fill-current' />
286335 </ DropdownMenuPrimitive . ItemIndicator >
287336 </ span >
288- { children }
337+ { withEllipsizedLabel ( children ) }
289338 </ DropdownMenuPrimitive . RadioItem >
290339) )
291340DropdownMenuRadioItem . displayName = DropdownMenuPrimitive . RadioItem . displayName
0 commit comments