2023-05-02 15:24:22 +04:00

211 lines
6.1 KiB
TypeScript

import React, {
useRef,
memo,
useEffect,
useLayoutEffect, useCallback,
} from '../../lib/teact/teact';
import { requestForcedReflow, requestMutation } from '../../lib/fasterdom/fasterdom';
import type { FC } from '../../lib/teact/teact';
import type { MenuItemContextAction } from './ListItem';
import { MouseButton } from '../../util/windowEnvironment';
import forceReflow from '../../util/forceReflow';
import buildClassName from '../../util/buildClassName';
import renderText from '../common/helpers/renderText';
import useMenuPosition from '../../hooks/useMenuPosition';
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import { useFastClick } from '../../hooks/useFastClick';
import Menu from './Menu';
import MenuItem from './MenuItem';
import MenuSeparator from './MenuSeparator';
import './Tab.scss';
type OwnProps = {
className?: string;
title: string;
isActive?: boolean;
isBlocked?: boolean;
badgeCount?: number;
isBadgeActive?: boolean;
previousActiveTab?: number;
onClick?: (arg: number) => void;
clickArg?: number;
contextActions?: MenuItemContextAction[];
contextRootElementSelector?: string;
};
const classNames = {
active: 'Tab--active',
badgeActive: 'Tab__badge--active',
};
const Tab: FC<OwnProps> = ({
className,
title,
isActive,
isBlocked,
badgeCount,
isBadgeActive,
previousActiveTab,
onClick,
clickArg,
contextActions,
contextRootElementSelector,
}) => {
// eslint-disable-next-line no-null/no-null
const tabRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
// Set initial active state
if (isActive && previousActiveTab === undefined && tabRef.current) {
tabRef.current!.classList.add(classNames.active);
}
}, [isActive, previousActiveTab]);
useEffect(() => {
if (!isActive || previousActiveTab === undefined) {
return;
}
const tabEl = tabRef.current!;
const prevTabEl = tabEl.parentElement!.children[previousActiveTab];
if (!prevTabEl) {
// The number of tabs in the parent component has decreased. It is necessary to add the active tab class name.
if (isActive && !tabEl.classList.contains(classNames.active)) {
requestMutation(() => {
tabEl.classList.add(classNames.active);
});
}
return;
}
const platformEl = tabEl.querySelector<HTMLElement>('.platform')!;
const prevPlatformEl = prevTabEl.querySelector<HTMLElement>('.platform')!;
// We move and resize the platform, so it repeats the position and size of the previous one
const shiftLeft = prevPlatformEl.parentElement!.offsetLeft - platformEl.parentElement!.offsetLeft;
const scaleFactor = prevPlatformEl.clientWidth / platformEl.clientWidth;
requestMutation(() => {
prevPlatformEl.classList.remove('animate');
platformEl.classList.remove('animate');
platformEl.style.transform = `translate3d(${shiftLeft}px, 0, 0) scale3d(${scaleFactor}, 1, 1)`;
requestForcedReflow(() => {
forceReflow(platformEl);
return () => {
platformEl.classList.add('animate');
platformEl.style.transform = 'none';
prevTabEl.classList.remove(classNames.active);
tabEl.classList.add(classNames.active);
};
});
});
}, [isActive, previousActiveTab]);
const {
contextMenuPosition, handleContextMenu, handleBeforeContextMenu, handleContextMenuClose,
handleContextMenuHide, isContextMenuOpen,
} = useContextMenuHandlers(tabRef, !contextActions);
const { handleClick, handleMouseDown } = useFastClick((e: React.MouseEvent<HTMLDivElement>) => {
if (contextActions && (e.button === MouseButton.Secondary || !onClick)) {
handleBeforeContextMenu(e);
}
if (e.type === 'mousedown' && e.button !== MouseButton.Main) {
return;
}
onClick?.(clickArg!);
});
const getTriggerElement = useCallback(() => tabRef.current, []);
const getRootElement = useCallback(
() => (contextRootElementSelector
? tabRef.current!.closest(contextRootElementSelector)
: document.body),
[contextRootElementSelector],
);
const getMenuElement = useCallback(
() => document.querySelector('#portals')!
.querySelector('.Tab-context-menu .bubble'),
[],
);
const getLayout = useCallback(
() => ({ withPortal: true }),
[],
);
const {
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
} = useMenuPosition(
contextMenuPosition,
getTriggerElement,
getRootElement,
getMenuElement,
getLayout,
);
return (
<div
className={buildClassName('Tab', onClick && 'Tab--interactive', className)}
onClick={handleClick}
onMouseDown={handleMouseDown}
onContextMenu={handleContextMenu}
ref={tabRef}
>
<span className="Tab_inner">
{renderText(title)}
{Boolean(badgeCount) && (
<span className={buildClassName('badge', isBadgeActive && classNames.badgeActive)}>{badgeCount}</span>
)}
{isBlocked && <i className="icon icon-lock-badge blocked" />}
<i className="platform" />
</span>
{contextActions && contextMenuPosition !== undefined && (
<Menu
isOpen={isContextMenuOpen}
transformOriginX={transformOriginX}
transformOriginY={transformOriginY}
positionX={positionX}
positionY={positionY}
style={menuStyle}
className="Tab-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>
);
};
export default memo(Tab);