parent
e112d3d9ed
commit
8d5858bab4
@ -1446,6 +1446,7 @@
|
||||
"MenuArchivedChats" = "Archived Chats";
|
||||
"MenuContacts" = "Contacts";
|
||||
"MenuSettings" = "Settings";
|
||||
"MenuMore" = "More";
|
||||
"MenuNightMode" = "Night Mode";
|
||||
"AriaMenuEnableNightMode" = "Enable night mode";
|
||||
"AriaMenuDisableNightMode" = "Disable night mode";
|
||||
|
||||
@ -57,7 +57,6 @@ const LeftSideMenuDropdown = ({
|
||||
return (
|
||||
<DropdownMenu
|
||||
trigger={trigger}
|
||||
footer={`${APP_NAME} ${versionString}`}
|
||||
className={buildClassName(
|
||||
'main-menu',
|
||||
lang.isRtl && 'rtl',
|
||||
@ -77,6 +76,7 @@ const LeftSideMenuDropdown = ({
|
||||
onSelectSettings={handleSelectSettings}
|
||||
onBotMenuOpened={markBotMenuOpen}
|
||||
onBotMenuClosed={unmarkBotMenuOpen}
|
||||
footer={`${APP_NAME} ${versionString}`}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
@ -37,6 +37,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import AttachBotItem from '../../middle/composer/AttachBotItem';
|
||||
import MenuItem from '../../ui/MenuItem';
|
||||
import MenuSeparator from '../../ui/MenuSeparator';
|
||||
import NestedMenuItem from '../../ui/NestedMenuItem';
|
||||
import Switcher from '../../ui/Switcher';
|
||||
import Toggle from '../../ui/Toggle';
|
||||
import AccountMenuItems from './AccountMenuItems';
|
||||
@ -47,6 +48,7 @@ type OwnProps = {
|
||||
onSelectArchived: NoneToVoidFunction;
|
||||
onBotMenuOpened: NoneToVoidFunction;
|
||||
onBotMenuClosed: NoneToVoidFunction;
|
||||
footer?: string;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -72,6 +74,7 @@ const LeftSideMenuItems = ({
|
||||
onSelectSettings,
|
||||
onBotMenuOpened,
|
||||
onBotMenuClosed,
|
||||
footer,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
openChat,
|
||||
@ -196,63 +199,74 @@ const LeftSideMenuItems = ({
|
||||
>
|
||||
{lang('MenuSettings')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="darkmode"
|
||||
onClick={handleDarkModeToggle}
|
||||
<NestedMenuItem
|
||||
icon="more"
|
||||
footer={footer}
|
||||
submenu={(
|
||||
<>
|
||||
<MenuItem
|
||||
icon="darkmode"
|
||||
onClick={handleDarkModeToggle}
|
||||
>
|
||||
<span className="menu-item-name">{lang('MenuNightMode')}</span>
|
||||
<Switcher
|
||||
id="darkmode"
|
||||
label={lang(theme === 'dark' ? 'AriaMenuDisableNightMode' : 'AriaMenuEnableNightMode')}
|
||||
checked={theme === 'dark'}
|
||||
noAnimation
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="animations"
|
||||
onClick={handleAnimationLevelChange}
|
||||
>
|
||||
<span className="menu-item-name capitalize">{lang('MenuAnimationsSwitch')}</span>
|
||||
<Toggle value={animationLevelValue} />
|
||||
</MenuItem>
|
||||
<MenuSeparator />
|
||||
<MenuItem
|
||||
icon="help"
|
||||
onClick={handleOpenTipsChat}
|
||||
>
|
||||
{lang('MenuTelegramFeatures')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="bug"
|
||||
onClick={handleBugReportClick}
|
||||
>
|
||||
{lang('MenuReportBug')}
|
||||
</MenuItem>
|
||||
{IS_BETA && (
|
||||
<MenuItem
|
||||
icon="permissions"
|
||||
onClick={handleChangelogClick}
|
||||
>
|
||||
{lang('MenuBetaChangelog')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{withOtherVersions && (
|
||||
<MenuItem
|
||||
icon="K"
|
||||
isCharIcon
|
||||
href={`${WEB_VERSION_BASE}k`}
|
||||
onClick={handleSwitchToWebK}
|
||||
>
|
||||
{lang('MenuSwitchToK')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{canInstall && (
|
||||
<MenuItem
|
||||
icon="install"
|
||||
onClick={getPromptInstall()}
|
||||
>
|
||||
{lang('MenuInstallApp')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<span className="menu-item-name">{lang('MenuNightMode')}</span>
|
||||
<Switcher
|
||||
id="darkmode"
|
||||
label={lang(theme === 'dark' ? 'AriaMenuDisableNightMode' : 'AriaMenuEnableNightMode')}
|
||||
checked={theme === 'dark'}
|
||||
noAnimation
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="animations"
|
||||
onClick={handleAnimationLevelChange}
|
||||
>
|
||||
<span className="menu-item-name capitalize">{lang('MenuAnimationsSwitch')}</span>
|
||||
<Toggle value={animationLevelValue} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="help"
|
||||
onClick={handleOpenTipsChat}
|
||||
>
|
||||
{lang('MenuTelegramFeatures')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="bug"
|
||||
onClick={handleBugReportClick}
|
||||
>
|
||||
{lang('MenuReportBug')}
|
||||
</MenuItem>
|
||||
{IS_BETA && (
|
||||
<MenuItem
|
||||
icon="permissions"
|
||||
onClick={handleChangelogClick}
|
||||
>
|
||||
{lang('MenuBetaChangelog')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{withOtherVersions && (
|
||||
<MenuItem
|
||||
icon="K"
|
||||
isCharIcon
|
||||
href={`${WEB_VERSION_BASE}k`}
|
||||
onClick={handleSwitchToWebK}
|
||||
>
|
||||
{lang('MenuSwitchToK')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{canInstall && (
|
||||
<MenuItem
|
||||
icon="install"
|
||||
onClick={getPromptInstall()}
|
||||
>
|
||||
{lang('MenuInstallApp')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{lang('MenuMore')}
|
||||
</NestedMenuItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -87,9 +87,10 @@
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 0.5rem 0;
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
font-size: 0.8125rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.25;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
|
||||
|
||||
@ -49,6 +49,7 @@ type OwnProps =
|
||||
onMouseEnterBackdrop?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onMouseLeave?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
withPortal?: boolean;
|
||||
nested?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
& MenuPositionOptions;
|
||||
@ -75,6 +76,7 @@ const Menu: FC<OwnProps> = ({
|
||||
onMouseLeave,
|
||||
withPortal,
|
||||
onMouseEnterBackdrop,
|
||||
nested,
|
||||
...positionOptions
|
||||
}) => {
|
||||
const { isTouchScreen } = useAppLayout();
|
||||
@ -108,12 +110,16 @@ const Menu: FC<OwnProps> = ({
|
||||
|
||||
const handleKeyDown = useKeyboardListNavigation(bubbleRef, isOpen, autoClose ? onClose : undefined, undefined, true);
|
||||
|
||||
const fullExcludedSelector = backdropExcludedSelector
|
||||
? `${backdropExcludedSelector}, .submenu`
|
||||
: '.submenu';
|
||||
|
||||
useVirtualBackdrop(
|
||||
isOpen,
|
||||
containerRef,
|
||||
noCloseOnBackdrop ? undefined : onClose,
|
||||
undefined,
|
||||
backdropExcludedSelector,
|
||||
fullExcludedSelector,
|
||||
);
|
||||
|
||||
const bubbleFullClassName = buildClassName(
|
||||
@ -147,7 +153,7 @@ const Menu: FC<OwnProps> = ({
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={isOpen ? onMouseLeave : undefined}
|
||||
>
|
||||
{isOpen && (
|
||||
{isOpen && !nested && (
|
||||
// This only prevents click events triggering on underlying elements
|
||||
<div
|
||||
className="backdrop"
|
||||
|
||||
@ -113,6 +113,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.submenu-icon {
|
||||
margin-inline: auto 0 !important;
|
||||
}
|
||||
|
||||
&.compact {
|
||||
will-change: transform;
|
||||
|
||||
|
||||
225
src/components/ui/NestedMenuItem.tsx
Normal file
225
src/components/ui/NestedMenuItem.tsx
Normal file
@ -0,0 +1,225 @@
|
||||
import {
|
||||
useEffect, useRef, useState, useUnmountCleanup,
|
||||
} from '@teact';
|
||||
|
||||
import type { IAnchorPosition } from '../../types';
|
||||
import type { IconName } from '../../types/icons';
|
||||
|
||||
import { requestMeasure } from '../../lib/fasterdom/fasterdom';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { REM } from '../common/helpers/mediaDimensions';
|
||||
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useUniqueId from '../../hooks/useUniqueId';
|
||||
import useWindowSize from '../../hooks/window/useWindowSize';
|
||||
|
||||
import Icon from '../common/icons/Icon';
|
||||
import Menu from './Menu';
|
||||
import MenuItem from './MenuItem';
|
||||
|
||||
const OPEN_TIMEOUT = 150;
|
||||
const CLOSE_TIMEOUT = 150;
|
||||
|
||||
type OwnProps = {
|
||||
icon?: IconName;
|
||||
customIcon?: React.ReactNode;
|
||||
submenuIcon?: IconName;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
submenu: React.ReactNode;
|
||||
submenuClassName?: string;
|
||||
disabled?: boolean;
|
||||
destructive?: boolean;
|
||||
ariaLabel?: string;
|
||||
footer?: string;
|
||||
};
|
||||
|
||||
const NestedMenuItem = ({
|
||||
icon,
|
||||
customIcon,
|
||||
submenuIcon,
|
||||
className,
|
||||
children,
|
||||
submenu,
|
||||
submenuClassName,
|
||||
disabled,
|
||||
destructive,
|
||||
ariaLabel,
|
||||
footer,
|
||||
}: OwnProps) => {
|
||||
const lang = useLang();
|
||||
|
||||
const itemRef = useRef<HTMLDivElement>();
|
||||
const closeTimeoutRef = useRef<number>();
|
||||
const openTimeoutRef = useRef<number>();
|
||||
const submenuId = useUniqueId();
|
||||
const isClosingRef = useRef(false);
|
||||
|
||||
const [isSubmenuOpen, openSubmenu, closeSubmenu] = useFlag(false);
|
||||
|
||||
useUnmountCleanup(() => {
|
||||
clearTimeout(closeTimeoutRef.current);
|
||||
clearTimeout(openTimeoutRef.current);
|
||||
});
|
||||
|
||||
const [submenuAnchor, setSubmenuAnchor] = useState<IAnchorPosition>();
|
||||
const { isResizing } = useWindowSize();
|
||||
|
||||
const updateAnchor = useLastCallback(() => {
|
||||
requestMeasure(() => {
|
||||
if (!itemRef.current) return;
|
||||
|
||||
const rect = itemRef.current.getBoundingClientRect();
|
||||
const overlap = REM;
|
||||
|
||||
setSubmenuAnchor({
|
||||
x: lang.isRtl ? rect.left + overlap : rect.right - overlap,
|
||||
y: rect.top,
|
||||
width: rect.width - overlap * 2,
|
||||
height: rect.height,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmenuOpen && !isResizing) {
|
||||
updateAnchor();
|
||||
}
|
||||
}, [isSubmenuOpen, lang.isRtl, updateAnchor, isResizing]);
|
||||
|
||||
const cancelOpen = useLastCallback(() => {
|
||||
clearTimeout(openTimeoutRef.current);
|
||||
openTimeoutRef.current = undefined;
|
||||
});
|
||||
|
||||
const cancelClose = useLastCallback(() => {
|
||||
clearTimeout(closeTimeoutRef.current);
|
||||
closeTimeoutRef.current = undefined;
|
||||
});
|
||||
|
||||
const scheduleOpen = useLastCallback(() => {
|
||||
cancelClose();
|
||||
cancelOpen();
|
||||
openTimeoutRef.current = window.setTimeout(() => {
|
||||
openTimeoutRef.current = undefined;
|
||||
// Don't open if the parent menu is closing
|
||||
const parentBubble = itemRef.current?.closest('.bubble');
|
||||
if (parentBubble?.classList.contains('closing')) return;
|
||||
openSubmenu();
|
||||
}, OPEN_TIMEOUT);
|
||||
});
|
||||
|
||||
const scheduleClose = useLastCallback(() => {
|
||||
cancelOpen();
|
||||
cancelClose();
|
||||
closeTimeoutRef.current = window.setTimeout(() => {
|
||||
closeSubmenu();
|
||||
closeTimeoutRef.current = undefined;
|
||||
}, CLOSE_TIMEOUT);
|
||||
});
|
||||
|
||||
const handleMouseEnter = useLastCallback(() => {
|
||||
if (disabled) return;
|
||||
scheduleOpen();
|
||||
});
|
||||
|
||||
const handleSubmenuMouseEnter = useLastCallback(() => {
|
||||
cancelOpen();
|
||||
cancelClose();
|
||||
});
|
||||
|
||||
const handleSubmenuMouseLeave = useLastCallback(() => {
|
||||
scheduleClose();
|
||||
});
|
||||
|
||||
const closeParentMenu = useLastCallback(() => {
|
||||
const parentMenu = itemRef.current?.closest('.Menu');
|
||||
if (parentMenu) {
|
||||
const backdrop = parentMenu.querySelector('.backdrop') as HTMLElement;
|
||||
if (backdrop) {
|
||||
const event = new MouseEvent('mousedown', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
});
|
||||
backdrop.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmenuClose = useLastCallback(() => {
|
||||
if (isClosingRef.current) return;
|
||||
isClosingRef.current = true;
|
||||
|
||||
cancelOpen();
|
||||
cancelClose();
|
||||
closeSubmenu();
|
||||
|
||||
closeParentMenu();
|
||||
|
||||
// Reset after a short delay
|
||||
setTimeout(() => {
|
||||
isClosingRef.current = false;
|
||||
}, 100);
|
||||
});
|
||||
|
||||
const getTriggerElement = useLastCallback(() => itemRef.current);
|
||||
const getRootElement = useLastCallback(() => document.body);
|
||||
const getMenuElement = useLastCallback(
|
||||
() => document.getElementById(submenuId)?.querySelector('.bubble') as HTMLElement | undefined,
|
||||
);
|
||||
const getLayout = useLastCallback(() => ({ withPortal: true }));
|
||||
|
||||
const handleClick = useLastCallback((e: React.SyntheticEvent<HTMLDivElement | HTMLAnchorElement>) => {
|
||||
e.stopPropagation();
|
||||
if (disabled || isSubmenuOpen) return;
|
||||
openSubmenu();
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={scheduleClose}
|
||||
ref={itemRef}
|
||||
>
|
||||
<MenuItem
|
||||
icon={icon}
|
||||
customIcon={customIcon}
|
||||
className={buildClassName(className, 'submenu')}
|
||||
disabled={disabled}
|
||||
destructive={destructive}
|
||||
ariaLabel={ariaLabel}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{children}
|
||||
<Icon name={submenuIcon || (lang.isRtl ? 'previous' : 'next')} className="submenu-icon" />
|
||||
{submenuAnchor && (
|
||||
<Menu
|
||||
id={submenuId}
|
||||
isOpen={isSubmenuOpen}
|
||||
className={buildClassName('submenu', submenuClassName)}
|
||||
anchor={submenuAnchor}
|
||||
positionX={lang.isRtl ? 'left' : 'right'}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
autoClose
|
||||
nested
|
||||
withPortal
|
||||
footer={footer}
|
||||
onClose={handleSubmenuClose}
|
||||
onMouseEnter={handleSubmenuMouseEnter}
|
||||
onMouseLeave={handleSubmenuMouseLeave}
|
||||
>
|
||||
{submenu}
|
||||
</Menu>
|
||||
)}
|
||||
</MenuItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NestedMenuItem;
|
||||
@ -132,6 +132,8 @@ function processDynamically(
|
||||
let { x, y } = anchor;
|
||||
const anchorX = x;
|
||||
const anchorY = y;
|
||||
const anchorWidth = anchor.width || 0;
|
||||
const anchorHeight = anchor.height || 0;
|
||||
|
||||
const menuEl = getMenuElement();
|
||||
const rootEl = getRootElement();
|
||||
@ -161,9 +163,9 @@ function processDynamically(
|
||||
if (isDense || (x + menuRect.width + extraPaddingX < rootRect.width + rootRect.left)) {
|
||||
x += 3;
|
||||
positionX = 'left';
|
||||
} else if (x - menuRect.width - rootRect.left > 0) {
|
||||
} else if (x - anchorWidth - menuRect.width - rootRect.left > 0) {
|
||||
positionX = 'right';
|
||||
x -= 3;
|
||||
x = x - anchorWidth - 3;
|
||||
} else {
|
||||
positionX = 'left';
|
||||
x = 16;
|
||||
@ -178,6 +180,7 @@ function processDynamically(
|
||||
y = yWithTopShift;
|
||||
} else {
|
||||
positionY = 'bottom';
|
||||
y = y + anchorHeight;
|
||||
|
||||
if (y - menuRect.height < rootRect.top + extraTopPadding) {
|
||||
y = rootRect.top + rootRect.height;
|
||||
|
||||
@ -163,6 +163,8 @@ export interface AccountSettings {
|
||||
export type IAnchorPosition = {
|
||||
x: number;
|
||||
y: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
export interface ShippingOption {
|
||||
|
||||
1
src/types/language.d.ts
vendored
1
src/types/language.d.ts
vendored
@ -1235,6 +1235,7 @@ export interface LangPair {
|
||||
'MenuArchivedChats': undefined;
|
||||
'MenuContacts': undefined;
|
||||
'MenuSettings': undefined;
|
||||
'MenuMore': undefined;
|
||||
'MenuNightMode': undefined;
|
||||
'AriaMenuEnableNightMode': undefined;
|
||||
'AriaMenuDisableNightMode': undefined;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user