@@ -23,6 +23,10 @@ export enum MenubarListItemRole {
2323 LISTBOX = 'listbox'
2424}
2525
26+ /* -------------------------------------------------------------------------------------------------
27+ * useMenuProps hook
28+ * -----------------------------------------------------------------------------------------------*/
29+
2630export function useMenuProps ( id : string ) {
2731 const activeMenu = useContext ( MenuOpenContext ) ;
2832
@@ -43,7 +47,7 @@ export function useMenuProps(id: string) {
4347 * -----------------------------------------------------------------------------------------------*/
4448
4549/** Custom subset of valid values for aria-hasPopup for the MenubarTrigger */
46- export enum MenubarTriggerAriaHasPopup {
50+ enum MenubarTriggerAriaHasPopup {
4751 MENU = MenubarListItemRole . MENU ,
4852 LISTBOX = MenubarListItemRole . LISTBOX ,
4953 TRUE = 'true'
@@ -94,6 +98,7 @@ const MenubarTrigger = React.forwardRef<HTMLButtonElement, MenubarTriggerProps>(
9498 const { id, title, first, last } = useContext ( SubmenuContext ) ;
9599 const { isOpen, handlers } = useMenuProps ( id ) ;
96100
101+ // `ref` is always a button from MenubarSubmenu, so safe to cast.
97102 const buttonRef = ref as React . RefObject < HTMLButtonElement > ;
98103
99104 const handleMouseEnter = ( e : React . MouseEvent ) => {
@@ -167,7 +172,7 @@ const MenubarTrigger = React.forwardRef<HTMLButtonElement, MenubarTriggerProps>(
167172 * MenubarList
168173 * -----------------------------------------------------------------------------------------------*/
169174
170- export interface MenubarListProps {
175+ interface MenubarListProps {
171176 /** MenubarItems that should be rendered in the list */
172177 children ?: React . ReactNode ;
173178 /** The ARIA role of the list element */
@@ -205,12 +210,26 @@ function MenubarList({
205210/* -------------------------------------------------------------------------------------------------
206211 * MenubarSubmenu
207212 * -----------------------------------------------------------------------------------------------*/
213+ /**
214+ * Safely casts a value to an HTMLElement.
215+ *
216+ * @param {unknown | null } node - The value to check.
217+ * @returns {HTMLElement | null } The node if it is an HTMLElement, otherwise null.
218+ */
219+ function getHTMLElement ( node : unknown | null ) : HTMLElement | null {
220+ return node instanceof HTMLElement ? node : null ;
221+ }
208222
209223export interface MenubarSubmenuProps {
224+ /** The unique id of the submenu */
210225 id : string ;
226+ /** A list of menu items that will be rendered in the menubar */
211227 children ?: React . ReactNode ;
228+ /** The title of the submenu */
212229 title : string ;
230+ /** The ARIA role of the trigger button */
213231 triggerRole ?: string ;
232+ /** The ARIA role of the list element */
214233 listRole ?: MenubarListItemRole ;
215234}
216235
@@ -219,14 +238,6 @@ export interface MenubarSubmenuProps {
219238 * that manages the state of the submenu and its items. It also provides keyboard navigation
220239 * and screen reader support. Supports menu and listbox roles. Needs to be a direct child of Menubar.
221240 *
222- * @param {Object } props
223- * @param {React.ReactNode } props.children - A list of menu items that will be rendered in the menubar
224- * @param {string } props.id - The unique id of the submenu
225- * @param {string } props.title - The title of the submenu
226- * @param {string } [props.triggerRole='menuitem'] - The ARIA role of the trigger button
227- * @param {string } [props.listRole='menu'] - The ARIA role of the list element
228- * @returns {JSX.Element }
229- *
230241 * @example
231242 * <Menubar>
232243 * <MenubarSubmenu id="file" title="File">
@@ -288,23 +299,22 @@ export function MenubarSubmenu({
288299 const items = Array . from ( submenuItems ) ;
289300 const activeItem = items [ submenuActiveIndex ] ;
290301
291- if ( activeItem ) {
292- const activeItemNode = activeItem . firstChild ;
302+ if ( ! activeItem ) return ;
293303
294- const isDisabled =
295- activeItemNode . getAttribute ( 'aria-disabled' ) === 'true' ;
304+ const activeItemNode = getHTMLElement ( activeItem . firstChild ) ;
296305
297- if ( isDisabled ) {
298- return ;
299- }
306+ if ( ! activeItemNode ) return ;
300307
301- activeItemNode . click ( ) ;
308+ const isDisabled = activeItemNode . getAttribute ( 'aria-disabled' ) === 'true' ;
302309
303- toggleMenuOpen ( id ) ;
310+ if ( isDisabled ) return ;
304311
305- if ( buttonRef . current ) {
306- buttonRef . current . focus ( ) ;
307- }
312+ activeItemNode . click ( ) ;
313+
314+ toggleMenuOpen ( id ) ;
315+
316+ if ( buttonRef . current ) {
317+ buttonRef . current . focus ( ) ;
308318 }
309319 } , [ submenuActiveIndex , submenuItems , buttonRef ] ) ;
310320
@@ -404,14 +414,13 @@ export function MenubarSubmenu({
404414 if ( isOpen && submenuItems . size > 0 ) {
405415 const items = Array . from ( submenuItems ) ;
406416 const activeItem = items [ submenuActiveIndex ] ;
417+ if ( ! activeItem ) return ;
407418
408- if ( activeItem ) {
409- const activeNode = activeItem . querySelector (
410- '[role="menuitem"], [role="option"]'
411- ) ;
412- if ( activeNode ) {
413- activeNode . focus ( ) ;
414- }
419+ const activeNode = getHTMLElement (
420+ activeItem . querySelector ( '[role="menuitem"], [role="option"]' )
421+ ) ;
422+ if ( activeNode ) {
423+ activeNode . focus ( ) ;
415424 }
416425 }
417426 } , [ isOpen , submenuItems , submenuActiveIndex ] ) ;
0 commit comments