import type { RefObject } from 'react'; import type { FC, TeactNode } from '../../lib/teact/teact'; import React, { useRef } from '../../lib/teact/teact'; import type { IconName } from '../../types/icons'; import { requestMeasure } from '../../lib/fasterdom/fasterdom'; import buildClassName from '../../util/buildClassName'; import { IS_TOUCH_ENV, MouseButton } from '../../util/windowEnvironment'; 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 useMenuPosition from '../../hooks/useMenuPosition'; import Icon from '../common/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?: RefObject; buttonRef?: RefObject; 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; multiline?: boolean; isStatic?: boolean; allowSelection?: boolean; withColorTransition?: boolean; contextActions?: MenuItemContextAction[]; withPortalForMenu?: boolean; menuBubbleClassName?: string; href?: string; onMouseDown?: (e: React.MouseEvent) => void; onClick?: (e: React.MouseEvent, arg?: any) => void; onContextMenu?: (e: React.MouseEvent) => void; clickArg?: any; onSecondaryIconClick?: (e: React.MouseEvent) => void; onDragEnter?: (e: React.DragEvent) => void; } const ListItem: FC = ({ ref, buttonRef, icon, iconClassName, leftElement, buttonClassName, menuBubbleClassName, secondaryIcon, secondaryIconClassName, rightElement, className, style, children, disabled, allowDisabledClick, ripple, narrow, inactive, focus, destructive, multiline, isStatic, allowSelection, withColorTransition, contextActions, withPortalForMenu, href, onMouseDown, onClick, onContextMenu, clickArg, onSecondaryIconClick, onDragEnter, }) => { // eslint-disable-next-line no-null/no-null let containerRef = useRef(null); if (ref) { containerRef = ref; } const [isTouched, markIsTouched, unmarkIsTouched] = useFlag(); const { isContextMenuOpen, contextMenuPosition, 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 { positionX, positionY, transformOriginX, transformOriginY, style: menuStyle, } = useMenuPosition( contextMenuPosition, getTriggerElement, getRootElement, getMenuElement, getLayout, ); const handleClickEvent = useLastCallback((e: React.MouseEvent) => { const hasModifierKey = e.ctrlKey || e.metaKey || e.shiftKey; if (!hasModifierKey && e.button === MouseButton.Main) { e.preventDefault(); } }); const handleClick = useLastCallback((e: React.MouseEvent) => { if ((disabled && !allowDisabledClick) || !onClick) { 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; } e.preventDefault(); } 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', contextMenuPosition && 'has-menu-open', focus && 'focus', destructive && 'destructive', 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 && ( )} {rightElement}
{contextActions && contextMenuPosition !== undefined && ( {contextActions.map((action) => ( ('isSeparator' in action) ? ( ) : ( {renderText(action.title)} ) ))} )}
); }; export default ListItem;