/* * Copyright (C) Contributors to the Suwayomi project * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /* * src: https://github.com/webzep/mui-nested-menu/blob/main/packages/mui-nested-menu/src/components/NestedMenuItem.tsx (2024-04-20 01:42) * * with a few changes to fix a bug on mobile devices where opening the sub menu immediately triggered the on click of the underlying menu item */ import Menu, { MenuProps as MuiMenuProps } from '@mui/material/Menu'; import { MenuItemProps as MuiMenuItemProps } from '@mui/material/MenuItem'; import { ElementType, forwardRef, HTMLAttributes, KeyboardEvent, FocusEvent, MouseEvent, ReactNode, RefAttributes, useImperativeHandle, useRef, useState, Ref, } from 'react'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; import Box from '@mui/material/Box'; import { OverridableComponent } from '@mui/material/OverridableComponent'; import { SvgIconTypeMap } from '@mui/material/SvgIcon'; import { IconMenuItem } from '@/modules/core/components/menu/IconMenuItem.tsx'; import { getOptionForDirection } from '@/theme.tsx'; import { MediaQuery } from '@/lib/ui/MediaQuery.tsx'; export type NestedMenuItemProps = Omit & { parentMenuOpen: boolean; component?: ElementType; label?: string; renderLabel?: () => ReactNode; RightIcon?: OverridableComponent & { muiName: string }; LeftIcon?: OverridableComponent & { muiName: string }; children?: ReactNode; className?: string; tabIndex?: number; disabled?: boolean; ContainerProps?: HTMLAttributes & RefAttributes; MenuProps?: Partial>; button?: true | undefined; }; const NestedMenuItem = forwardRef((props, ref) => { const { parentMenuOpen, label, renderLabel, RightIcon = getOptionForDirection(ChevronRightIcon, ChevronLeftIcon), LeftIcon, children, className, tabIndex: tabIndexProp, ContainerProps: ContainerPropsProp = {}, MenuProps, ...MenuItemProps } = props; const isTouchDevice = MediaQuery.useIsTouchDevice(); const { ref: containerRefProp, ...ContainerProps } = ContainerPropsProp; const menuItemRef = useRef(null); useImperativeHandle(ref, () => menuItemRef.current!); const containerRef = useRef(null); useImperativeHandle(containerRefProp as Ref, () => containerRef.current as HTMLElement); const menuContainerRef = useRef(null); const [isSubMenuOpen, setIsSubMenuOpen] = useState(false); const changeMenuOpenState = (open: boolean) => { if (isSubMenuOpen === open) { return; } if (props.disabled) { setIsSubMenuOpen(false); return; } setIsSubMenuOpen(open); }; const handleMouseEnter = (e: MouseEvent) => { if (isTouchDevice) { return; } changeMenuOpenState(true); if (ContainerProps.onMouseEnter) { ContainerProps.onMouseEnter(e); } }; const handleMouseLeave = (e: MouseEvent) => { changeMenuOpenState(false); if (ContainerProps.onMouseLeave) { ContainerProps.onMouseLeave(e); } }; // Check if any immediate children are active const isSubmenuFocused = () => { const active = containerRef.current?.ownerDocument.activeElement ?? null; if (menuContainerRef.current == null) { return false; } for (const child of menuContainerRef.current.children) { if (child === active) { return true; } } return false; }; const handleFocus = (e: FocusEvent) => { if (isTouchDevice) { return; } if (e.target === containerRef.current) { changeMenuOpenState(true); } if (ContainerProps.onFocus) { ContainerProps.onFocus(e); } }; const handleClick = (e: MouseEvent) => { changeMenuOpenState(!isSubMenuOpen); if (ContainerProps.onClick) { ContainerProps.onClick(e); } }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { return; } if (isSubmenuFocused()) { e.stopPropagation(); } const active = containerRef.current?.ownerDocument.activeElement; if (e.key === 'ArrowLeft' && isSubmenuFocused()) { containerRef.current?.focus(); } if (e.key === 'ArrowRight' && e.target === containerRef.current && e.target === active) { const firstChild = menuContainerRef.current?.children[0] as HTMLDivElement; firstChild?.focus(); } }; const open = isSubMenuOpen && parentMenuOpen; // Root element must have a `tabIndex` attribute for keyboard navigation let tabIndex; if (!props.disabled) { tabIndex = tabIndexProp !== undefined ? tabIndexProp : -1; } return ( { changeMenuOpenState(false); }} {...MenuProps} > {children} ); }); NestedMenuItem.displayName = 'NestedMenuItem'; export { NestedMenuItem };