211 lines
6.1 KiB
TypeScript
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);
|