Skip to content

Commit 2427c1f

Browse files
committed
MenubarSubmenu: fix final typeerrors by adding htmlElement check
1 parent 913d9e6 commit 2427c1f

File tree

1 file changed

+38
-29
lines changed

1 file changed

+38
-29
lines changed

client/components/Menubar/MenubarSubmenu.tsx

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export enum MenubarListItemRole {
2323
LISTBOX = 'listbox'
2424
}
2525

26+
/* -------------------------------------------------------------------------------------------------
27+
* useMenuProps hook
28+
* -----------------------------------------------------------------------------------------------*/
29+
2630
export 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

209223
export 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

Comments
 (0)