import type { ElementRef, TeactNode } from '../../lib/teact/teact'; import type React from '../../lib/teact/teact'; import { useRef } from '../../lib/teact/teact'; import type { IconName } from '../../types/icons'; import { requestMeasure } from '../../lib/fasterdom/fasterdom'; import { IS_TOUCH_ENV, MouseButton } from '../../util/browser/windowEnvironment'; import buildClassName from '../../util/buildClassName'; import renderText from '../common/helpers/renderText'; import useContextMenuHandlers from '../../hooks/useContextMenuHandlers'; import { useFastClick } from '../../hooks/useFastClick'; import useFlag from '../../hooks/useFlag'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import Icon from '../common/icons/Icon'; import Button from './Button'; import Menu from './Menu'; import MenuItem from './MenuItem'; import MenuSeparator from './MenuSeparator'; import RippleEffect from './RippleEffect'; import './ListItem.scss'; type MenuItemContextActionItem = { title: string; icon: IconName; destructive?: boolean; handler?: () => void; }; type MenuItemContextActionSeparator = { isSeparator: true; key?: string; }; export type MenuItemContextAction = MenuItemContextActionItem | MenuItemContextActionSeparator; interface OwnProps { ref?: ElementRef; buttonRef?: ElementRef; icon?: IconName; iconClassName?: string; leftElement?: TeactNode; secondaryIcon?: IconName; secondaryIconClassName?: string; rightElement?: TeactNode; buttonClassName?: string; className?: string; style?: string; children: React.ReactNode; disabled?: boolean; allowDisabledClick?: boolean; ripple?: boolean; narrow?: boolean; inactive?: boolean; focus?: boolean; destructive?: boolean; withPrimaryColor?: boolean; multiline?: boolean; isStatic?: boolean; allowSelection?: boolean; withColorTransition?: boolean; contextActions?: MenuItemContextAction[]; withPortalForMenu?: boolean; menuBubbleClassName?: string; href?: string; nonInteractive?: boolean; onClick?: (e: React.MouseEvent, arg?: any) => void; clickArg?: any; onMouseDown?: (e: React.MouseEvent) => void; onContextMenu?: (e: React.MouseEvent) => void; onSecondaryIconClick?: (e: React.MouseEvent) => void; onDragEnter?: (e: React.DragEvent) => void; onDragLeave?: NoneToVoidFunction; } const ListItem = ({ ref, buttonRef, icon, iconClassName, leftElement, buttonClassName, menuBubbleClassName, secondaryIcon, secondaryIconClassName, rightElement, className, style, children, disabled, allowDisabledClick, ripple, narrow, inactive, focus, destructive, withPrimaryColor, multiline, isStatic, allowSelection, withColorTransition, contextActions, withPortalForMenu, href, nonInteractive, onClick, clickArg, onMouseDown, onContextMenu, onSecondaryIconClick, onDragEnter, onDragLeave, }: OwnProps) => { let containerRef = useRef(); if (ref) { containerRef = ref; } const [isTouched, markIsTouched, unmarkIsTouched] = useFlag(); const { isContextMenuOpen, contextMenuAnchor, handleBeforeContextMenu, handleContextMenu, handleContextMenuClose, handleContextMenuHide, } = useContextMenuHandlers(containerRef, !contextActions); const getTriggerElement = useLastCallback(() => containerRef.current); const getRootElement = useLastCallback(() => containerRef.current!.closest('.custom-scroll')); const getMenuElement = useLastCallback(() => { return (withPortalForMenu ? document.querySelector('#portals') : containerRef.current)! .querySelector('.ListItem-context-menu .bubble'); }); const getLayout = useLastCallback(() => ({ withPortal: withPortalForMenu })); const handleClickEvent = useLastCallback((e: React.MouseEvent) => { const hasModifierKey = e.ctrlKey || e.metaKey || e.shiftKey; if (!hasModifierKey && e.button === MouseButton.Main) { if (href && !onClick) return; // Allow default behavior for opening links e.preventDefault(); } }); const handleClick = useLastCallback((e: React.MouseEvent) => { if ((disabled && !allowDisabledClick)) { return; } if (href) { // Allow default behavior for opening links in new tab const hasModifierKey = e.ctrlKey || e.metaKey || e.shiftKey; if ((hasModifierKey && e.button === MouseButton.Main) || e.button === MouseButton.Auxiliary) { return; } if (onClick) e.preventDefault(); } if (!onClick) return; onClick(e, clickArg); if (IS_TOUCH_ENV && !ripple) { markIsTouched(); requestMeasure(unmarkIsTouched); } }); const { handleClick: handleSecondaryIconClick, handleMouseDown: handleSecondaryIconMouseDown, } = useFastClick((e: React.MouseEvent) => { if ((disabled && !allowDisabledClick) || e.button !== 0 || (!onSecondaryIconClick && !contextActions)) return; e.stopPropagation(); if (onSecondaryIconClick) { onSecondaryIconClick(e); } else { handleContextMenu(e); } }); const handleMouseDown = useLastCallback((e: React.MouseEvent) => { if (inactive || IS_TOUCH_ENV) { return; } if (contextActions && (e.button === MouseButton.Secondary || !onClick)) { handleBeforeContextMenu(e); } if (e.button === MouseButton.Main) { if (!onClick) { handleContextMenu(e); } else { handleClick(e); } } }); const lang = useLang(); const fullClassName = buildClassName( 'ListItem', className, allowSelection && 'allow-selection', ripple && 'has-ripple', narrow && 'narrow', disabled && 'disabled', allowDisabledClick && 'click-allowed', inactive && 'inactive', contextMenuAnchor && 'has-menu-open', focus && 'focus', destructive && 'destructive', withPrimaryColor && 'primary', multiline && 'multiline', isStatic && 'is-static', withColorTransition && 'with-color-transition', ); const ButtonElementTag = href ? 'a' : 'div'; return (
{!disabled && !inactive && ripple && ( )} {leftElement} {icon && ( )} {multiline && (
{children}
)} {!multiline && children} {secondaryIcon && (
); }; export default ListItem;