@@ -42,12 +42,46 @@ const MENU_ROW_RADIUS_CLASS = 'rounded-lg'
4242
4343/**
4444 * Rows are a fixed height, so a label that wraps overflows its row and paints
45- * over its neighbours instead of growing the row. Keep every row on one line:
46- * a bare text label is clipped at the surface edge, and a label wrapped in a
47- * `<span>` (the shape long or count-bearing labels should use) ellipsizes.
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}.
4847 */
4948const MENU_ROW_SINGLE_LINE_CLASS = 'whitespace-nowrap [&>span]:min-w-0 [&>span]:truncate'
5049
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+
5185/**
5286 * Surface corner, shared by the root menu and submenus — they previously
5387 * disagreed, at 12px and 8px.
@@ -124,7 +158,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
124158 ) }
125159 { ...props }
126160 >
127- { children }
161+ { withEllipsizedLabel ( children ) }
128162 < ChevronRight className = 'ml-auto size-[14px] shrink-0' />
129163 </ DropdownMenuPrimitive . SubTrigger >
130164 )
@@ -193,7 +227,8 @@ const DropdownMenuItem = React.forwardRef<
193227 */
194228 action ?: React . ReactNode
195229 }
196- > ( ( { className, inset, action, ...props } , ref ) => {
230+ > ( ( { className, inset, action, asChild, children, ...props } , ref ) => {
231+ const content = asChild ? children : withEllipsizedLabel ( children )
197232 if ( action ) {
198233 return (
199234 < div className = 'group/dropdownitem relative' >
@@ -205,8 +240,11 @@ const DropdownMenuItem = React.forwardRef<
205240 inset && 'pl-7' ,
206241 className
207242 ) }
243+ asChild = { asChild }
208244 { ...props }
209- />
245+ >
246+ { content }
247+ </ DropdownMenuPrimitive . Item >
210248 < 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' >
211249 { action }
212250 </ div >
@@ -217,8 +255,11 @@ const DropdownMenuItem = React.forwardRef<
217255 < DropdownMenuPrimitive . Item
218256 ref = { ref }
219257 className = { cn ( DROPDOWN_MENU_ITEM_BASE_CLASSES , inset && 'pl-7' , className ) }
258+ asChild = { asChild }
220259 { ...props }
221- />
260+ >
261+ { content }
262+ </ DropdownMenuPrimitive . Item >
222263 )
223264} )
224265DropdownMenuItem . displayName = DropdownMenuPrimitive . Item . displayName
@@ -260,7 +301,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
260301 < DropdownMenuPrimitive . CheckboxItem
261302 ref = { ref }
262303 className = { cn (
263- `relative flex ${ MENU_ROW_HEIGHT_CLASS } 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` ,
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` ,
264305 className
265306 ) }
266307 checked = { checked }
@@ -271,7 +312,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
271312 < Check className = 'size-[14px]' />
272313 </ DropdownMenuPrimitive . ItemIndicator >
273314 </ span >
274- { children }
315+ { withEllipsizedLabel ( children ) }
275316 </ DropdownMenuPrimitive . CheckboxItem >
276317) )
277318DropdownMenuCheckboxItem . displayName = DropdownMenuPrimitive . CheckboxItem . displayName
@@ -283,7 +324,7 @@ const DropdownMenuRadioItem = React.forwardRef<
283324 < DropdownMenuPrimitive . RadioItem
284325 ref = { ref }
285326 className = { cn (
286- `relative flex ${ MENU_ROW_HEIGHT_CLASS } 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` ,
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` ,
287328 className
288329 ) }
289330 { ...props }
@@ -293,7 +334,7 @@ const DropdownMenuRadioItem = React.forwardRef<
293334 < Circle className = 'size-[6px] fill-current' />
294335 </ DropdownMenuPrimitive . ItemIndicator >
295336 </ span >
296- { children }
337+ { withEllipsizedLabel ( children ) }
297338 </ DropdownMenuPrimitive . RadioItem >
298339) )
299340DropdownMenuRadioItem . displayName = DropdownMenuPrimitive . RadioItem . displayName
0 commit comments