@@ -28,16 +28,14 @@ type ChildProps =
2828 width : number
2929 groupId ?: string
3030 }
31+ | { type : 'divider' | 'group' ; width : number }
3132 | {
32- type : 'menuItem'
33+ type : 'menu'
34+ width : number
3335 label : string
34- disabled : boolean
35- icon ?: ActionBarIconButtonProps [ 'icon' ]
36- onClick : MouseEventHandler
37- menuId ?: string
36+ icon : ActionBarIconButtonProps [ 'icon' ]
37+ items : ActionBarMenuProps [ 'items' ]
3838 }
39- | { type : 'divider' | 'group' ; width : number }
40- | { type : 'menu' ; width : number ; label : string ; icon : ActionBarIconButtonProps [ 'icon' ] }
4139
4240/**
4341 * Registry of descendants to render in the list or menu. To preserve insertion order across updates, children are
@@ -271,18 +269,15 @@ export const ActionBar: React.FC<React.PropsWithChildren<ActionBarProps>> = prop
271269
272270 const groupedItems = React . useMemo ( ( ) => {
273271 const groupedItemsMap = new Map < string , Array < [ string , ChildProps ] > > ( )
274- const menuItems = new Map < string , ChildProps > ( )
275272
276273 for ( const [ key , childProps ] of childRegistry ) {
277274 if ( childProps ?. type === 'action' && childProps . groupId ) {
278275 const existingGroup = groupedItemsMap . get ( childProps . groupId ) || [ ]
279276 existingGroup . push ( [ key , childProps ] )
280277 groupedItemsMap . set ( childProps . groupId , existingGroup )
281- } else if ( childProps ?. type === 'menuItem' ) {
282- menuItems . set ( key , childProps )
283278 }
284279 }
285- return { groupedItems : groupedItemsMap , menuItems }
280+ return groupedItemsMap
286281 } , [ childRegistry ] )
287282
288283 return (
@@ -331,11 +326,8 @@ export const ActionBar: React.FC<React.PropsWithChildren<ActionBarProps>> = prop
331326 )
332327 }
333328
334- // Use the memoized map instead of filtering each time
335- const groupedMenuItems = groupedItems . groupedItems . get ( id ) || [ ]
336-
337- if ( menuItem . type === 'menu' ) {
338- const menuItems = Array . from ( groupedItems . menuItems )
329+ if ( menuItem . type === 'menu' && menuItem . items ) {
330+ const menuItems = menuItem . items
339331 const { icon : Icon , label} = menuItem
340332
341333 return (
@@ -350,15 +342,20 @@ export const ActionBar: React.FC<React.PropsWithChildren<ActionBarProps>> = prop
350342 </ ActionMenu . Anchor >
351343 < ActionMenu . Overlay >
352344 < ActionList >
353- { menuItems . map ( ( [ key , childProps ] ) => (
354- < ActionList . Item key = { key } > { childProps . label } </ ActionList . Item >
345+ { menuItems . map ( ( { label, onClick, disabled, variant} ) => (
346+ < ActionList . Item key = { label } onSelect = { onClick } disabled = { disabled } variant = { variant } >
347+ { label }
348+ </ ActionList . Item >
355349 ) ) }
356350 </ ActionList >
357351 </ ActionMenu . Overlay >
358352 </ ActionMenu >
359353 )
360354 }
361355
356+ // Use the memoized map instead of filtering each time
357+ const groupedMenuItems = groupedItems . get ( id ) || [ ]
358+
362359 // If we ever add additional types, this condition will be necessary
363360 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
364361 if ( menuItem . type === 'group' ) {
@@ -408,7 +405,6 @@ export const ActionBarIconButton = forwardRef(
408405
409406 const { size, registerChild, unregisterChild, isVisibleChild} = React . useContext ( ActionBarContext )
410407 const { groupId} = React . useContext ( ActionBarGroupContext )
411- const { menuId} = React . useContext ( ActionBarMenuContext )
412408
413409 // Storing the width in a ref ensures we don't forget about it when not visible
414410 const widthRef = useRef < number > ( )
@@ -419,7 +415,7 @@ export const ActionBarIconButton = forwardRef(
419415 if ( ! widthRef . current ) return
420416
421417 registerChild ( id , {
422- type : menuId ? 'menuItem' : 'action' ,
418+ type : 'action' ,
423419 label : props [ 'aria-label' ] ?? '' ,
424420 icon : props . icon ,
425421 disabled : ! ! disabled ,
@@ -492,11 +488,19 @@ export const ActionBarGroup = forwardRef(({children}: React.PropsWithChildren, f
492488 )
493489} )
494490
491+ type ActionBarMenuItem = {
492+ disabled ?: boolean
493+ icon ?: ActionBarIconButtonProps [ 'icon' ]
494+ label : string
495+ onClick ?: ActionListItemProps [ 'onSelect' ]
496+ } & Pick < ActionListItemProps , 'variant' >
497+
495498type ActionBarMenuProps = {
496499 /** Accessible label for the menu button */
497- 'aria-label' : string
500+ 'aria-label' : string // TODO: Change to label
498501 /** Icon for the menu button */
499502 icon : ActionBarIconButtonProps [ 'icon' ]
503+ items ?: ActionBarMenuItem [ ]
500504}
501505
502506const ActionBarMenuContext = React . createContext < {
@@ -505,107 +509,53 @@ const ActionBarMenuContext = React.createContext<{
505509 label : string
506510} > ( { menuId : '' , menuVisible : false , label : '' } )
507511
508- export const ActionBarMenu = forwardRef (
509- ( { 'aria-label' : ariaLabel , icon, children} : React . PropsWithChildren < ActionBarMenuProps > , forwardedRef ) => {
510- const backupRef = useRef < HTMLButtonElement > ( null )
511- const ref = ( forwardedRef ?? backupRef ) as RefObject < HTMLButtonElement >
512- const id = useId ( )
513- const { registerChild, unregisterChild, isVisibleChild} = React . useContext ( ActionBarContext )
514-
515- const [ menuOpen , setMenuOpen ] = useState ( false )
516-
517- // Like IconButton, we store the width in a ref to ensure that we don't forget about it when not visible
518- // If a child has a groupId, it won't be visible if the group isn't visible, so we don't need to check isVisibleChild here
519- const widthRef = useRef < number > ( )
520-
521- useIsomorphicLayoutEffect ( ( ) => {
522- const width = ref . current ?. getBoundingClientRect ( ) . width
523- if ( width ) widthRef . current = width
524-
525- if ( ! widthRef . current ) return
526-
527- registerChild ( id , { type : 'menu' , width : widthRef . current , label : ariaLabel , icon} )
528-
529- return ( ) => {
530- unregisterChild ( id )
531- }
532- } , [ registerChild , unregisterChild ] )
533-
534- if ( ! isVisibleChild ( id ) )
535- return (
536- < ActionBarMenuContext . Provider value = { { menuId : id , menuVisible : isVisibleChild ( id ) , label : ariaLabel } } >
537- { children }
538- </ ActionBarMenuContext . Provider >
539- )
540-
541- return (
542- < ActionBarMenuContext . Provider value = { { menuId : id , menuVisible : isVisibleChild ( id ) , label : ariaLabel } } >
543- < ActionMenu anchorRef = { ref } open = { menuOpen } onOpenChange = { setMenuOpen } >
544- < ActionMenu . Anchor >
545- < IconButton variant = "invisible" aria-label = { ariaLabel } icon = { icon } />
546- </ ActionMenu . Anchor >
547- < ActionMenu . Overlay >
548- < ActionList className = { styles . Menu } > { children } </ ActionList >
549- </ ActionMenu . Overlay >
550- </ ActionMenu >
551- </ ActionBarMenuContext . Provider >
552- )
553- } ,
554- )
555-
556- type ActionBarMenuItemProps = {
557- disabled ?: boolean
558- icon ?: ActionBarIconButtonProps [ 'icon' ]
559- label : string
560- } & ActionListItemProps
561-
562- export const ActionBarMenuItem = forwardRef (
563- (
564- { disabled, children, icon : Icon , label, ...props } : React . PropsWithChildren < ActionBarMenuItemProps > ,
565- forwardedRef ,
566- ) => {
567- const backupRef = useRef < HTMLLIElement > ( null )
568- const ref = ( forwardedRef ?? backupRef ) as RefObject < HTMLLIElement >
569- useRefObjectAsForwardedRef ( forwardedRef , ref )
570- const id = useId ( )
512+ export const ActionBarMenu = forwardRef ( ( { 'aria-label' : ariaLabel , icon, items} : ActionBarMenuProps , forwardedRef ) => {
513+ const backupRef = useRef < HTMLButtonElement > ( null )
514+ const ref = ( forwardedRef ?? backupRef ) as RefObject < HTMLButtonElement >
515+ const id = useId ( )
516+ const { registerChild, unregisterChild, isVisibleChild} = React . useContext ( ActionBarContext )
571517
572- const { menuVisible} = React . useContext ( ActionBarMenuContext )
573- const { registerChild, unregisterChild} = React . useContext ( ActionBarContext )
518+ const [ menuOpen , setMenuOpen ] = useState ( false )
574519
575- // TODO: We need to support an assortment of ActionList.Item props like variant, etc.
576- // We do not want to reinvent the wheel, so it should be simplistic to pass those props through
520+ // Like IconButton, we store the width in a ref to ensure that we don't forget about it when not visible
521+ // If a child has a groupId, it won't be visible if the group isn't visible, so we don't need to check isVisibleChild here
522+ const widthRef = useRef < number > ( )
577523
578- useIsomorphicLayoutEffect ( ( ) => {
579- if ( menuVisible ) return
524+ useIsomorphicLayoutEffect ( ( ) => {
525+ const width = ref . current ?. getBoundingClientRect ( ) . width
526+ if ( width ) widthRef . current = width
580527
581- registerChild ( id , {
582- type : 'menuItem' ,
583- label,
584- icon : Icon ,
585- disabled : ! ! disabled ,
586- onClick : props . onClick as MouseEventHandler ,
587- } )
528+ if ( ! widthRef . current ) return
588529
589- return ( ) => {
590- unregisterChild ( id )
591- }
592- } , [ registerChild , unregisterChild ] )
530+ registerChild ( id , { type : 'menu' , width : widthRef . current , label : ariaLabel , icon, items} )
593531
594- if ( ! menuVisible ) {
595- // We return null here as there is no need to render anything when the menu is not visible
596- // We instead register the item in the ActionBar context for the ActionBar to render it appropriately in the overflow menu
597- return null
532+ return ( ) => {
533+ unregisterChild ( id )
598534 }
535+ } , [ registerChild , unregisterChild ] )
599536
600- return (
601- < ActionList . Item aria-disabled = { disabled } ref = { ref } data-testid = { id } >
602- < ActionList . LeadingVisual > { Icon ? < Icon /> : null } </ ActionList . LeadingVisual >
603- { label }
604- { children }
605- </ ActionList . Item >
606- )
607- } ,
608- )
537+ if ( ! isVisibleChild ( id ) ) return null
538+
539+ return (
540+ < ActionBarMenuContext . Provider value = { { menuId : id , menuVisible : isVisibleChild ( id ) , label : ariaLabel } } >
541+ < ActionMenu anchorRef = { ref } open = { menuOpen } onOpenChange = { setMenuOpen } >
542+ < ActionMenu . Anchor >
543+ < IconButton variant = "invisible" aria-label = { ariaLabel } icon = { icon } />
544+ </ ActionMenu . Anchor >
545+ < ActionMenu . Overlay >
546+ < ActionList className = { styles . Menu } >
547+ { items ?. map ( ( { label, onClick, disabled, icon : MenuIcon , variant} ) => (
548+ < ActionList . Item key = { label } onSelect = { onClick } disabled = { disabled } variant = { variant } >
549+ { MenuIcon && < ActionList . LeadingVisual > { < MenuIcon /> } </ ActionList . LeadingVisual > }
550+ { label }
551+ </ ActionList . Item >
552+ ) ) }
553+ </ ActionList >
554+ </ ActionMenu . Overlay >
555+ </ ActionMenu >
556+ </ ActionBarMenuContext . Provider >
557+ )
558+ } )
609559
610560export const VerticalDivider = ( ) => {
611561 const ref = useRef < HTMLDivElement > ( null )
0 commit comments