Profile: Add Focus Message button for entities in tabs (#6949)

This commit is contained in:
Alexander Zinchuk 2026-05-15 18:37:57 +02:00
parent 480b09e9aa
commit 33c8b7d958
11 changed files with 315 additions and 14 deletions

View File

@ -3,6 +3,12 @@
display: flex;
align-items: flex-start;
&.has-menu-open {
border-radius: var(--border-radius-default);
background-color: var(--color-chat-hover);
box-shadow: 0 0 0 0.5rem var(--color-chat-hover);
}
&.inline {
margin-top: calc(0.5rem - 0.3125rem);

View File

@ -12,6 +12,7 @@ import type { BufferedRange } from '../../hooks/useBuffering';
import type { OldLangFn } from '../../hooks/useOldLang';
import type { ThemeKey } from '../../types';
import type { LangFn } from '../../util/localization';
import type { MenuItemContextAction } from '../ui/ListItem';
import { ApiMediaFormat } from '../../api/types';
import { AudioOrigin } from '../../types';
@ -38,6 +39,7 @@ import { MAX_EMPTY_WAVEFORM_POINTS, renderWaveform } from './helpers/waveform';
import useAppLayout from '../../hooks/useAppLayout';
import useAudioPlayer from '../../hooks/useAudioPlayer';
import useBuffering from '../../hooks/useBuffering';
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import useMedia from '../../hooks/useMedia';
@ -47,6 +49,9 @@ import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated
import Button from '../ui/Button';
import Link from '../ui/Link';
import Menu from '../ui/Menu';
import MenuItem from '../ui/MenuItem';
import MenuSeparator from '../ui/MenuSeparator';
import ProgressSpinner from '../ui/ProgressSpinner';
import AnimatedFileSize from './AnimatedFileSize';
import AnimatedIcon from './AnimatedIcon';
@ -79,6 +84,7 @@ type OwnProps = {
onReadMedia?: () => void;
onCancelUpload?: () => void;
onDateClick?: (arg: ApiMessage) => void;
contextActions?: MenuItemContextAction[];
};
type StateProps = {
@ -119,6 +125,7 @@ const Audio = ({
onReadMedia,
onCancelUpload,
onDateClick,
contextActions,
}: OwnProps & StateProps) => {
const {
cancelMediaDownload, downloadMedia, transcribeAudio, openOneTimeMediaModal,
@ -133,6 +140,8 @@ const Audio = ({
const media = (voice || video || audio)!;
const mediaSource = (voice || video);
const isVoice = Boolean(voice || video);
const containerRef = useRef<HTMLDivElement>();
const menuRef = useRef<HTMLDivElement>();
const isSeekingRef = useRef<boolean>(false);
const seekerRef = useRef<HTMLDivElement>();
const oldLang = useOldLang();
@ -265,6 +274,17 @@ const Audio = ({
}
});
const {
isContextMenuOpen, contextMenuAnchor,
handleBeforeContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(containerRef, !contextActions);
const getTriggerElement = useLastCallback(() => containerRef.current);
const getRootElement = useLastCallback(() => containerRef.current!.closest('.custom-scroll') || document.body);
const getMenuElement = useLastCallback(() => menuRef.current);
const getLayout = useLastCallback(() => ({ withPortal: true }));
const handleSeek = useLastCallback((e: MouseEvent | TouchEvent) => {
if (isSeekingRef.current && seekerRef.current) {
const { width, left } = seekerRef.current.getBoundingClientRect();
@ -343,6 +363,7 @@ const Audio = ({
isOwn && origin === AudioOrigin.Inline && 'own',
(origin === AudioOrigin.Search || origin === AudioOrigin.SharedMedia) && 'bigger',
isSelected && 'audio-is-selected',
contextMenuAnchor && 'has-menu-open',
);
const buttonClassNames = ['toogle-play-wrapper'];
@ -420,7 +441,13 @@ const Audio = ({
}
return (
<div className={fullClassName} dir={lang.isRtl ? 'rtl' : 'ltr'}>
<div
ref={containerRef}
className={fullClassName}
dir={lang.isRtl ? 'rtl' : 'ltr'}
onMouseDown={handleBeforeContextMenu}
onContextMenu={contextActions ? handleContextMenu : undefined}
>
{isSelectable && (
<div className="message-select-control no-selection">
{isSelected && <Icon name="check" className="message-select-control-icon" />}
@ -492,6 +519,38 @@ const Audio = ({
origin,
)
)}
{contextActions && contextMenuAnchor !== undefined && (
<Menu
ref={menuRef}
isOpen={isContextMenuOpen}
anchor={contextMenuAnchor}
getTriggerElement={getTriggerElement}
getRootElement={getRootElement}
getMenuElement={getMenuElement}
getLayout={getLayout}
className="shared-media-context-menu"
autoClose
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
withPortal
>
{contextActions.map((action) => (
('isSeparator' in action) ? (
<MenuSeparator key={action.key || 'separator'} />
) : (
<MenuItem
key={action.title}
icon={action.icon}
destructive={action.destructive}
disabled={!action.handler}
onClick={action.handler}
>
{action.title}
</MenuItem>
)
))}
</Menu>
)}
</div>
);
};

View File

@ -5,6 +5,7 @@ import { getActions } from '../../global';
import type { ApiDocument, ApiMessage, MediaContent } from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import type { MenuItemContextAction } from '../ui/ListItem';
import {
getDocumentMediaHash,
@ -42,6 +43,7 @@ type OwnProps = {
shouldWarnAboutFiles?: boolean;
id?: string;
onCancelUpload?: NoneToVoidFunction;
contextActions?: MenuItemContextAction[];
} & ({
message: ApiMessage;
onDateClick: (arg: ApiMessage) => void;
@ -73,12 +75,13 @@ const Document = ({
onCancelUpload,
onMediaClick,
onDateClick,
contextActions,
}: OwnProps) => {
const { cancelMediaDownload, downloadMedia, setSharedSettingOption } = getActions();
const ref = useRef<HTMLDivElement>();
const lang = useOldLang();
const oldLang = useOldLang();
const [isFileIpDialogOpen, openFileIpDialog, closeFileIpDialog] = useFlag();
const [shouldNotWarnAboutFiles, setShouldNotWarnAboutFiles] = useState(false);
@ -209,6 +212,7 @@ const Document = ({
isSelectable={isSelectable}
isSelected={isSelected}
actionIcon={withMediaViewer ? (isDocumentVideo(document) ? 'play' : 'eye') : 'download'}
contextActions={contextActions}
onClick={handleClick}
onDateClick={onDateClick ? handleDateClick : undefined}
/>
@ -217,11 +221,11 @@ const Document = ({
onClose={closeFileIpDialog}
confirmHandler={handleFileIpConfirm}
>
{lang('lng_launch_svg_warning')}
{oldLang('lng_launch_svg_warning')}
<Checkbox
className="dialog-checkbox"
checked={shouldNotWarnAboutFiles}
label={lang('lng_launch_exe_dont_ask')}
label={oldLang('lng_launch_exe_dont_ask')}
onCheck={setShouldNotWarnAboutFiles}
/>
</ConfirmDialog>

View File

@ -1,6 +1,7 @@
.File {
--secondary-color: var(--color-text-secondary);
position: relative;
display: flex;
align-items: center;
@ -140,6 +141,12 @@
}
}
&.has-menu-open {
border-radius: var(--border-radius-default);
background-color: var(--color-chat-hover);
box-shadow: 0 0 0 0.5rem var(--color-chat-hover);
}
.file-info {
overflow: hidden;
flex-grow: 1;

View File

@ -6,6 +6,7 @@ import {
import type { ApiAttachment, MediaContent } from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import type { IconName } from '../../types/icons';
import type { MenuItemContextAction } from '../ui/ListItem';
import buildClassName from '../../util/buildClassName';
import { formatMediaDateTime, formatPastTimeShort } from '../../util/dates/oldDateFormat';
@ -13,11 +14,16 @@ import { getColorFromExtension } from './helpers/documentInfo';
import { getDocumentThumbnailDimensions } from './helpers/mediaDimensions';
import renderText from './helpers/renderText';
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated';
import Link from '../ui/Link';
import Menu from '../ui/Menu';
import MenuItem from '../ui/MenuItem';
import MenuSeparator from '../ui/MenuSeparator';
import ProgressSpinner from '../ui/ProgressSpinner';
import AnimatedFileSize from './AnimatedFileSize';
import CompactMediaPreview, { canRenderCompactMediaPreview } from './CompactMediaPreview';
@ -46,6 +52,7 @@ type OwnProps = {
isSelected?: boolean;
transferProgress?: number;
actionIcon?: IconName;
contextActions?: MenuItemContextAction[];
onClick?: () => void;
onDateClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
};
@ -68,6 +75,7 @@ const File = ({
isSelected,
transferProgress,
actionIcon,
contextActions,
observeIntersection,
onClick,
onDateClick,
@ -78,6 +86,7 @@ const File = ({
if (ref) {
elementRef = ref;
}
const menuRef = useRef<HTMLDivElement>();
const {
shouldRender: shouldSpinnerRender,
@ -86,6 +95,17 @@ const File = ({
const color = getColorFromExtension(extension);
const {
isContextMenuOpen, contextMenuAnchor,
handleBeforeContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(elementRef, !contextActions);
const getTriggerElement = useLastCallback(() => elementRef.current);
const getRootElement = useLastCallback(() => elementRef.current!.closest('.custom-scroll') || document.body);
const getMenuElement = useLastCallback(() => menuRef.current);
const getLayout = useLastCallback(() => ({ withPortal: true }));
const { width } = getDocumentThumbnailDimensions(previewSize);
const shouldRenderPreview = canRenderCompactMediaPreview(previewMedia, previewAttachment);
@ -95,10 +115,18 @@ const File = ({
previewSize !== 'medium' && `size-${previewSize}`,
onClick && !isUploading && 'interactive',
isSelected && 'file-is-selected',
contextMenuAnchor && 'has-menu-open',
);
return (
<div id={id} ref={elementRef} className={fullClassName} dir={lang.isRtl ? 'rtl' : undefined}>
<div
id={id}
ref={elementRef}
className={fullClassName}
dir={lang.isRtl ? 'rtl' : undefined}
onMouseDown={handleBeforeContextMenu}
onContextMenu={contextActions ? handleContextMenu : undefined}
>
{isSelectable && (
<div className="message-select-control no-selection">
{isSelected && <Icon name="check" className="message-select-control-icon" />}
@ -157,6 +185,38 @@ const File = ({
{sender && Boolean(timestamp) && (
<Link onClick={onDateClick}>{formatPastTimeShort(oldLang, timestamp * 1000)}</Link>
)}
{contextActions && contextMenuAnchor !== undefined && (
<Menu
ref={menuRef}
isOpen={isContextMenuOpen}
anchor={contextMenuAnchor}
getTriggerElement={getTriggerElement}
getRootElement={getRootElement}
getMenuElement={getMenuElement}
getLayout={getLayout}
className="shared-media-context-menu"
autoClose
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
withPortal
>
{contextActions.map((action) => (
('isSeparator' in action) ? (
<MenuSeparator key={action.key || 'separator'} />
) : (
<MenuItem
key={action.title}
icon={action.icon}
destructive={action.destructive}
disabled={!action.handler}
onClick={action.handler}
>
{action.title}
</MenuItem>
)
))}
</Menu>
)}
</div>
);
};

View File

@ -8,6 +8,25 @@
height: 0;
padding-bottom: 100%;
&::after {
pointer-events: none;
content: "";
position: absolute;
z-index: 2;
inset: 0;
opacity: 0;
background: rgba(0, 0, 0, 0.16);
box-shadow: inset 0 0 0 0.125rem var(--color-primary);
transition: opacity 0.15s ease-in-out;
}
&.has-menu-open::after {
opacity: 1;
}
.video-duration {
position: absolute;
top: 0.3125rem;

View File

@ -2,6 +2,7 @@ import { memo, useRef } from '../../lib/teact/teact';
import type { ApiMessage } from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import type { MenuItemContextAction } from '../ui/ListItem';
import {
getMessageHtmlId,
@ -16,12 +17,16 @@ import stopEvent from '../../util/stopEvent';
import useMessageMediaHash from '../../hooks/media/useMessageMediaHash';
import useThumbnail from '../../hooks/media/useThumbnail';
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import useFlag from '../../hooks/useFlag';
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
import useLastCallback from '../../hooks/useLastCallback';
import useMedia from '../../hooks/useMedia';
import useMediaTransitionDeprecated from '../../hooks/useMediaTransitionDeprecated';
import Menu from '../ui/Menu';
import MenuItem from '../ui/MenuItem';
import MenuSeparator from '../ui/MenuSeparator';
import OptimizedVideo from '../ui/OptimizedVideo';
import MediaSpoiler from './MediaSpoiler';
@ -34,6 +39,7 @@ type OwnProps = {
canAutoPlay?: boolean;
observeIntersection?: ObserveFn;
onClick?: (messageId: number, chatId: string) => void;
contextActions?: MenuItemContextAction[];
};
const Media = ({
@ -43,8 +49,10 @@ const Media = ({
canAutoPlay,
observeIntersection,
onClick,
contextActions,
}: OwnProps) => {
const ref = useRef<HTMLDivElement>();
const menuRef = useRef<HTMLDivElement>();
const isIntersecting = useIsIntersecting(ref, observeIntersection);
const [isHovering, markMouseOver, markMouseOut] = useFlag();
@ -61,7 +69,20 @@ const Media = ({
const hasSpoiler = getMessageIsSpoiler(message);
const [isSpoilerShown, , hideSpoiler] = useFlag(hasSpoiler);
const {
isContextMenuOpen, contextMenuAnchor,
handleBeforeContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(ref, !contextActions);
const getTriggerElement = useLastCallback(() => ref.current);
const getRootElement = useLastCallback(() => ref.current!.closest('.custom-scroll') || document.body);
const getMenuElement = useLastCallback(() => menuRef.current);
const getLayout = useLastCallback(() => ({ withPortal: true }));
const handleClick = useLastCallback(() => {
if (isContextMenuOpen) return;
hideSpoiler();
onClick!(message.id, message.chatId);
});
@ -70,10 +91,12 @@ const Media = ({
<div
ref={ref}
id={`${idPrefix}${getMessageHtmlId(message.id)}`}
className="Media scroll-item"
className={buildClassName('Media scroll-item', contextMenuAnchor && 'has-menu-open')}
onClick={onClick ? handleClick : undefined}
onMouseDown={handleBeforeContextMenu}
onMouseOver={!IS_TOUCH_ENV ? markMouseOver : undefined}
onMouseOut={!IS_TOUCH_ENV ? markMouseOut : undefined}
onContextMenu={contextActions ? handleContextMenu : undefined}
>
<img
src={thumbDataUri}
@ -81,7 +104,7 @@ const Media = ({
alt=""
draggable={!isProtected}
decoding="async"
onContextMenu={isProtected ? stopEvent : undefined}
onContextMenu={isProtected && !contextActions ? stopEvent : undefined}
/>
{fullGifBlobUrl ? (
<OptimizedVideo
@ -93,7 +116,7 @@ const Media = ({
playsInline
draggable={false}
disablePictureInPicture
onContextMenu={isProtected ? stopEvent : undefined}
onContextMenu={isProtected && !contextActions ? stopEvent : undefined}
/>
) : (
<img
@ -102,7 +125,7 @@ const Media = ({
alt=""
draggable={false}
decoding="async"
onContextMenu={isProtected ? stopEvent : undefined}
onContextMenu={isProtected && !contextActions ? stopEvent : undefined}
/>
)}
{hasSpoiler && (
@ -114,6 +137,38 @@ const Media = ({
)}
{video && <span className="video-duration">{video.isGif ? 'GIF' : formatMediaDuration(video.duration)}</span>}
{isProtected && <span className="protector" />}
{contextActions && contextMenuAnchor !== undefined && (
<Menu
ref={menuRef}
isOpen={isContextMenuOpen}
anchor={contextMenuAnchor}
getTriggerElement={getTriggerElement}
getRootElement={getRootElement}
getMenuElement={getMenuElement}
getLayout={getLayout}
className="shared-media-context-menu"
autoClose
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
withPortal
>
{contextActions.map((action) => (
('isSeparator' in action) ? (
<MenuSeparator key={action.key || 'separator'} />
) : (
<MenuItem
key={action.title}
icon={action.icon}
destructive={action.destructive}
disabled={!action.handler}
onClick={action.handler}
>
{action.title}
</MenuItem>
)
))}
</Menu>
)}
</div>
);
};

View File

@ -11,6 +11,12 @@
margin-top: 1.5rem;
}
&.has-menu-open {
border-radius: var(--border-radius-default);
background-color: var(--color-chat-hover);
box-shadow: 0 0 0 0.5rem var(--color-chat-hover);
}
&.without-media::before {
content: attr(data-initial);

View File

@ -1,9 +1,10 @@
import { memo, useMemo } from '../../lib/teact/teact';
import { memo, useMemo, useRef } from '../../lib/teact/teact';
import { withGlobal } from '../../global';
import type { ApiMessage, ApiWebPage } from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import type { TextPart } from '../../types';
import type { MenuItemContextAction } from '../ui/ListItem';
import {
getFirstLinkInMessage,
@ -16,11 +17,15 @@ import trimText from '../../util/trimText';
import { renderMessageSummary } from './helpers/renderMessageText';
import renderText from './helpers/renderText';
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import Link from '../ui/Link';
import Menu from '../ui/Menu';
import MenuItem from '../ui/MenuItem';
import MenuSeparator from '../ui/MenuSeparator';
import Media from './Media';
import SafeLink from './SafeLink';
@ -37,6 +42,7 @@ type OwnProps = {
senderTitle?: string;
isProtected?: boolean;
observeIntersection?: ObserveFn;
contextActions?: MenuItemContextAction[];
onMessageClick: (message: ApiMessage) => void;
};
@ -45,8 +51,10 @@ type StateProps = {
};
const WebLink = ({
message, webPage, senderTitle, isProtected, observeIntersection, onMessageClick,
message, webPage, senderTitle, isProtected, observeIntersection, contextActions, onMessageClick,
}: OwnProps & StateProps) => {
const ref = useRef<HTMLDivElement>();
const menuRef = useRef<HTMLDivElement>();
const lang = useLang();
const oldLang = useOldLang();
@ -54,6 +62,17 @@ const WebLink = ({
onMessageClick(message);
});
const {
isContextMenuOpen, contextMenuAnchor,
handleBeforeContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(ref, !contextActions, true);
const getTriggerElement = useLastCallback(() => ref.current);
const getRootElement = useLastCallback(() => ref.current!.closest('.custom-scroll') || document.body);
const getMenuElement = useLastCallback(() => menuRef.current);
const getLayout = useLastCallback(() => ({ withPortal: true }));
let linkData: ApiWebPageWithFormatted | undefined = webPage;
if (!linkData) {
@ -114,18 +133,27 @@ const WebLink = ({
const className = buildClassName(
'WebLink scroll-item',
(!photo && !video) && 'without-media',
contextMenuAnchor && 'has-menu-open',
);
const safeLinkContent = displayUrl || url.replace('mailto:', '');
return (
<div
ref={ref}
className={className}
data-initial={siteTitle[0]}
dir={lang.isRtl ? 'rtl' : undefined}
onMouseDown={handleBeforeContextMenu}
onContextMenu={contextActions ? handleContextMenu : undefined}
>
{photo && (
<Media message={message} isProtected={isProtected} observeIntersection={observeIntersection} />
<Media
message={message}
isProtected={isProtected}
observeIntersection={observeIntersection}
contextActions={contextActions}
/>
)}
<div className="content">
<Link isRtl={lang.isRtl} className="site-title" onClick={handleMessageClick}>
@ -155,6 +183,38 @@ const WebLink = ({
</Link>
</div>
)}
{contextActions && contextMenuAnchor !== undefined && (
<Menu
ref={menuRef}
isOpen={isContextMenuOpen}
anchor={contextMenuAnchor}
getTriggerElement={getTriggerElement}
getRootElement={getRootElement}
getMenuElement={getMenuElement}
getLayout={getLayout}
className="shared-media-context-menu"
autoClose
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
withPortal
>
{contextActions.map((action) => (
('isSeparator' in action) ? (
<MenuSeparator key={action.key || 'separator'} />
) : (
<MenuItem
key={action.title}
icon={action.icon}
destructive={action.destructive}
disabled={!action.handler}
onClick={action.handler}
>
{action.title}
</MenuItem>
)
))}
</Menu>
)}
</div>
);
};

View File

@ -710,6 +710,16 @@ const Profile = ({
focusMessage({ chatId: message.chatId, messageId: message.id });
});
const getMessageContextActions = useLastCallback((message: ApiMessage): MenuItemContextAction[] => {
return [{
title: lang('FocusMessage'),
icon: 'show-message',
handler: () => {
handleMessageFocus(message);
},
}];
});
const handleDeleteMembersModalClose = useLastCallback(() => {
setDeletingUserId(undefined);
});
@ -1023,6 +1033,7 @@ const Profile = ({
canAutoPlay={canAutoPlayGifs}
observeIntersection={observeIntersectionForMedia}
onClick={handleSelectMedia}
contextActions={getMessageContextActions(messagesById[id])}
/>
))
) : (resultType === 'stories' || resultType === 'storiesArchive') ? (
@ -1049,6 +1060,7 @@ const Profile = ({
message={messagesById[id]}
shouldWarnAboutFiles={shouldWarnAboutFiles}
onMediaClick={handleSelectMedia}
contextActions={getMessageContextActions(messagesById[id])}
/>
))
) : resultType === 'links' ? (
@ -1058,6 +1070,7 @@ const Profile = ({
message={messagesById[id]}
isProtected={isChatProtected || messagesById[id].isProtected}
observeIntersection={observeIntersectionForMedia}
contextActions={getMessageContextActions(messagesById[id])}
onMessageClick={handleMessageFocus}
/>
))
@ -1072,6 +1085,7 @@ const Profile = ({
className="scroll-item"
onPlay={handlePlayAudio}
onDateClick={handleMessageFocus}
contextActions={getMessageContextActions(messagesById[id])}
canDownload={!isChatProtected && !messagesById[id].isProtected}
isDownloading={getIsDownloading(activeDownloads, messagesById[id].content.audio!)}
/>
@ -1093,6 +1107,7 @@ const Profile = ({
className="scroll-item"
onPlay={handlePlayAudio}
onDateClick={handleMessageFocus}
contextActions={getMessageContextActions(message)}
canDownload={!isChatProtected && !message.isProtected}
isDownloading={getIsDownloading(activeDownloads, media)}
/>

View File

@ -18,6 +18,16 @@ function stopEvent(e: Event) {
e.stopPropagation();
}
function isNativeLinkTarget(target: EventTarget | undefined) {
if (!(target instanceof HTMLElement)) {
return false;
}
const link = target.closest('a[href]');
return Boolean(link && link.getAttribute('href') !== '#');
}
const useContextMenuHandlers = (
elementRef: ElementRef<HTMLElement>,
isMenuDisabled?: boolean,
@ -43,7 +53,7 @@ const useContextMenuHandlers = (
removeExtraClass(e.target as HTMLElement, 'no-selection');
});
if (isMenuDisabled || (shouldDisableOnLink && (e.target as HTMLElement).matches('a[href]'))) {
if (isMenuDisabled || (shouldDisableOnLink && isNativeLinkTarget(e.target))) {
return;
}
e.preventDefault();
@ -91,7 +101,7 @@ const useContextMenuHandlers = (
const { clientX, clientY, target } = originalEvent.touches[0];
if (contextMenuAnchor || (shouldDisableOnLink && (target as HTMLElement).matches('a[href]'))) {
if (contextMenuAnchor || (shouldDisableOnLink && isNativeLinkTarget(target))) {
return;
}