This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
@@ -0,0 +1,10 @@
import * as React from 'react';
import type { EditableConfig, TabsLocale } from '../interface';
export interface AddButtonProps {
prefixCls: string;
editable?: EditableConfig;
locale?: TabsLocale;
style?: React.CSSProperties;
}
declare const AddButton: React.ForwardRefExoticComponent<AddButtonProps & React.RefAttributes<HTMLButtonElement>>;
export default AddButton;
@@ -0,0 +1,25 @@
import * as React from 'react';
const AddButton = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
prefixCls,
editable,
locale,
style
} = props;
if (!editable || editable.showAdd === false) {
return null;
}
return /*#__PURE__*/React.createElement("button", {
ref: ref,
type: "button",
className: `${prefixCls}-nav-add`,
style: style,
"aria-label": locale?.addAriaLabel || 'Add tab',
onClick: event => {
editable.onEdit('add', {
event
});
}
}, editable.addIcon || '+');
});
export default AddButton;
@@ -0,0 +1,9 @@
import * as React from 'react';
import type { TabBarExtraContent, TabBarExtraPosition } from '../interface';
interface ExtraContentProps {
position: TabBarExtraPosition;
prefixCls: string;
extra?: TabBarExtraContent;
}
declare const ExtraContent: React.ForwardRefExoticComponent<ExtraContentProps & React.RefAttributes<HTMLDivElement>>;
export default ExtraContent;
@@ -0,0 +1,34 @@
import * as React from 'react';
const ExtraContent = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
position,
prefixCls,
extra
} = props;
if (!extra) {
return null;
}
let content;
// Parse extra
let assertExtra = {};
if (typeof extra === 'object' && ! /*#__PURE__*/React.isValidElement(extra)) {
assertExtra = extra;
} else {
assertExtra.right = extra;
}
if (position === 'right') {
content = assertExtra.right;
}
if (position === 'left') {
content = assertExtra.left;
}
return content ? /*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-extra-content`,
ref: ref
}, content) : null;
});
if (process.env.NODE_ENV !== 'production') {
ExtraContent.displayName = 'ExtraContent';
}
export default ExtraContent;
@@ -0,0 +1,24 @@
import * as React from 'react';
import type { EditableConfig, Tab, TabsLocale, MoreProps } from '../interface';
export interface OperationNodeProps {
prefixCls: string;
className?: string;
style?: React.CSSProperties;
id: string;
tabs: Tab[];
rtl: boolean;
tabBarGutter?: number;
activeKey: string;
mobile: boolean;
more?: MoreProps;
editable?: EditableConfig;
locale?: TabsLocale;
removeAriaLabel?: string;
onTabClick: (key: string, e: React.MouseEvent | React.KeyboardEvent) => void;
tabMoving?: boolean;
getPopupContainer?: (node: HTMLElement) => HTMLElement;
popupClassName?: string;
popupStyle?: React.CSSProperties;
}
declare const _default: React.MemoExoticComponent<React.ForwardRefExoticComponent<OperationNodeProps & React.RefAttributes<HTMLDivElement>>>;
export default _default;
@@ -0,0 +1,191 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import { clsx } from 'clsx';
import Dropdown from '@rc-component/dropdown';
import Menu, { MenuItem } from '@rc-component/menu';
import KeyCode from "@rc-component/util/es/KeyCode";
import * as React from 'react';
import { useEffect, useState } from 'react';
import { getRemovable } from "../util";
import AddButton from "./AddButton";
const OperationNode = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
prefixCls,
id,
tabs,
locale,
mobile,
more: moreProps = {},
style,
className,
editable,
tabBarGutter,
rtl,
removeAriaLabel,
onTabClick,
getPopupContainer,
popupClassName,
popupStyle
} = props;
// ======================== Dropdown ========================
const [open, setOpen] = useState(false);
const [selectedKey, setSelectedKey] = useState(null);
const {
icon: moreIcon = 'More'
} = moreProps;
const popupId = `${id}-more-popup`;
const dropdownPrefix = `${prefixCls}-dropdown`;
const selectedItemId = selectedKey !== null ? `${popupId}-${selectedKey}` : null;
const dropdownAriaLabel = locale?.dropdownAriaLabel;
function onRemoveTab(event, key) {
event.preventDefault();
event.stopPropagation();
editable.onEdit('remove', {
key,
event
});
}
const menu = /*#__PURE__*/React.createElement(Menu, {
onClick: ({
key,
domEvent
}) => {
onTabClick(key, domEvent);
setOpen(false);
},
prefixCls: `${dropdownPrefix}-menu`,
id: popupId,
tabIndex: -1,
role: "listbox",
"aria-activedescendant": selectedItemId,
selectedKeys: [selectedKey],
"aria-label": dropdownAriaLabel !== undefined ? dropdownAriaLabel : 'expanded dropdown'
}, tabs.map(tab => {
const {
closable,
disabled,
closeIcon,
key,
label
} = tab;
const removable = getRemovable(closable, closeIcon, editable, disabled);
return /*#__PURE__*/React.createElement(MenuItem, {
key: key,
id: `${popupId}-${key}`,
role: "option",
"aria-controls": id && `${id}-panel-${key}`,
disabled: disabled
}, /*#__PURE__*/React.createElement("span", null, label), removable && /*#__PURE__*/React.createElement("button", {
type: "button",
"aria-label": removeAriaLabel || 'remove',
tabIndex: 0,
className: `${dropdownPrefix}-menu-item-remove`,
onClick: e => {
e.stopPropagation();
onRemoveTab(e, key);
}
}, closeIcon || editable.removeIcon || '×'));
}));
function selectOffset(offset) {
const enabledTabs = tabs.filter(tab => !tab.disabled);
let selectedIndex = enabledTabs.findIndex(tab => tab.key === selectedKey) || 0;
const len = enabledTabs.length;
for (let i = 0; i < len; i += 1) {
selectedIndex = (selectedIndex + offset + len) % len;
const tab = enabledTabs[selectedIndex];
if (!tab.disabled) {
setSelectedKey(tab.key);
return;
}
}
}
function onKeyDown(e) {
const {
which
} = e;
if (!open) {
if ([KeyCode.DOWN, KeyCode.SPACE, KeyCode.ENTER].includes(which)) {
setOpen(true);
e.preventDefault();
}
return;
}
switch (which) {
case KeyCode.UP:
selectOffset(-1);
e.preventDefault();
break;
case KeyCode.DOWN:
selectOffset(1);
e.preventDefault();
break;
case KeyCode.ESC:
setOpen(false);
break;
case KeyCode.SPACE:
case KeyCode.ENTER:
if (selectedKey !== null) {
onTabClick(selectedKey, e);
}
break;
}
}
// ========================= Effect =========================
useEffect(() => {
// We use query element here to avoid React strict warning
const ele = document.getElementById(selectedItemId);
if (ele?.scrollIntoView) {
ele.scrollIntoView(false);
}
}, [selectedItemId, selectedKey]);
useEffect(() => {
if (!open) {
setSelectedKey(null);
}
}, [open]);
// ========================= Render =========================
const moreStyle = {
marginInlineStart: tabBarGutter
};
if (!tabs.length) {
moreStyle.visibility = 'hidden';
moreStyle.order = 1;
}
const overlayClassName = clsx(popupClassName, {
[`${dropdownPrefix}-rtl`]: rtl
});
const moreNode = mobile ? null : /*#__PURE__*/React.createElement(Dropdown, _extends({
prefixCls: dropdownPrefix,
overlay: menu,
visible: tabs.length ? open : false,
onVisibleChange: setOpen,
overlayClassName: overlayClassName,
overlayStyle: popupStyle,
mouseEnterDelay: 0.1,
mouseLeaveDelay: 0.1,
getPopupContainer: getPopupContainer
}, moreProps), /*#__PURE__*/React.createElement("button", {
type: "button",
className: `${prefixCls}-nav-more`,
style: moreStyle,
"aria-haspopup": "listbox",
"aria-controls": popupId,
id: `${id}-more`,
"aria-expanded": open,
onKeyDown: onKeyDown
}, moreIcon));
return /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-nav-operations`, className),
style: style,
ref: ref
}, moreNode, /*#__PURE__*/React.createElement(AddButton, {
prefixCls: prefixCls,
locale: locale,
editable: editable
}));
});
export default /*#__PURE__*/React.memo(OperationNode, (_, next) =>
// https://github.com/ant-design/ant-design/issues/32544
// We'd better remove syntactic sugar in `rc-menu` since this has perf issue
next.tabMoving);
@@ -0,0 +1,27 @@
import * as React from 'react';
import type { EditableConfig, Tab } from '../interface';
export interface TabNodeProps {
id: string;
prefixCls: string;
tab: Tab;
active: boolean;
focus: boolean;
closable?: boolean;
editable?: EditableConfig;
onClick?: (e: React.MouseEvent | React.KeyboardEvent) => void;
onResize?: (width: number, height: number, left: number, top: number) => void;
renderWrapper?: (node: React.ReactElement) => React.ReactElement;
removeAriaLabel?: string;
tabCount: number;
currentPosition: number;
removeIcon?: React.ReactNode;
onKeyDown: React.KeyboardEventHandler;
onMouseDown: React.MouseEventHandler;
onMouseUp: React.MouseEventHandler;
onFocus: React.FocusEventHandler;
onBlur: React.FocusEventHandler;
style?: React.CSSProperties;
className?: string;
}
declare const TabNode: React.FC<TabNodeProps>;
export default TabNode;
@@ -0,0 +1,107 @@
import { clsx } from 'clsx';
import * as React from 'react';
import { genDataNodeKey, getRemovable } from "../util";
const TabNode = props => {
const {
prefixCls,
id,
active,
focus,
tab: {
key,
label,
disabled,
closeIcon,
icon
},
closable,
renderWrapper,
removeAriaLabel,
editable,
onClick,
onFocus,
onBlur,
onKeyDown,
onMouseDown,
onMouseUp,
style,
className,
tabCount,
currentPosition
} = props;
const tabPrefix = `${prefixCls}-tab`;
const removable = getRemovable(closable, closeIcon, editable, disabled);
function onInternalClick(e) {
if (disabled) {
return;
}
onClick(e);
}
function onRemoveTab(event) {
event.preventDefault();
event.stopPropagation();
editable.onEdit('remove', {
key,
event
});
}
const labelNode = React.useMemo(() => icon && typeof label === 'string' ? /*#__PURE__*/React.createElement("span", null, label) : label, [label, icon]);
const btnRef = React.useRef(null);
React.useEffect(() => {
if (focus && btnRef.current) {
btnRef.current.focus();
}
}, [focus]);
const node = /*#__PURE__*/React.createElement("div", {
key: key,
"data-node-key": genDataNodeKey(key),
className: clsx(tabPrefix, className, {
[`${tabPrefix}-with-remove`]: removable,
[`${tabPrefix}-active`]: active,
[`${tabPrefix}-disabled`]: disabled,
[`${tabPrefix}-focus`]: focus
}),
style: style,
onClick: onInternalClick
}, /*#__PURE__*/React.createElement("div", {
ref: btnRef,
role: "tab",
"aria-selected": active,
id: id && `${id}-tab-${key}`,
className: `${tabPrefix}-btn`,
"aria-controls": id && `${id}-panel-${key}`,
"aria-disabled": disabled,
tabIndex: disabled ? null : active ? 0 : -1,
onClick: e => {
e.stopPropagation();
onInternalClick(e);
},
onKeyDown: onKeyDown,
onMouseDown: onMouseDown,
onMouseUp: onMouseUp,
onFocus: onFocus,
onBlur: onBlur
}, focus && /*#__PURE__*/React.createElement("div", {
"aria-live": "polite",
style: {
width: 0,
height: 0,
position: 'absolute',
overflow: 'hidden',
opacity: 0
}
}, `Tab ${currentPosition} of ${tabCount}`), icon && /*#__PURE__*/React.createElement("span", {
className: `${tabPrefix}-icon`
}, icon), label && labelNode), removable && /*#__PURE__*/React.createElement("button", {
type: "button",
"aria-label": removeAriaLabel || 'remove',
tabIndex: active ? 0 : -1,
className: `${tabPrefix}-remove`,
onClick: e => {
e.stopPropagation();
onRemoveTab(e);
}
}, closeIcon || editable.removeIcon || '×'));
return renderWrapper ? renderWrapper(node) : node;
};
export default TabNode;
@@ -0,0 +1,5 @@
import * as React from 'react';
import type { TabNavListProps } from '.';
export type TabNavListWrapperProps = Required<Omit<TabNavListProps, 'children' | 'className'>> & TabNavListProps;
declare const TabNavListWrapper: React.FC<TabNavListWrapperProps>;
export default TabNavListWrapper;
@@ -0,0 +1,18 @@
// zombieJ: To compatible with `renderTabBar` usage.
import * as React from 'react';
import TabNavList from '.';
// We have to create a TabNavList components.
const TabNavListWrapper = ({
renderTabBar,
...restProps
}) => {
if (renderTabBar) {
return renderTabBar(restProps, TabNavList);
}
return /*#__PURE__*/React.createElement(TabNavList, restProps);
};
if (process.env.NODE_ENV !== 'production') {
TabNavListWrapper.displayName = 'TabNavListWrapper';
}
export default TabNavListWrapper;
@@ -0,0 +1,33 @@
import * as React from 'react';
import type { GetIndicatorSize } from '../hooks/useIndicator';
import type { AnimatedConfig, EditableConfig, MoreProps, OnTabScroll, RenderTabBar, TabBarExtraContent, TabPosition, TabsLocale } from '../interface';
import type { SemanticName } from '../Tabs';
export interface TabNavListProps {
id: string;
tabPosition: TabPosition;
activeKey: string;
rtl: boolean;
animated?: AnimatedConfig;
extra?: TabBarExtraContent;
editable?: EditableConfig;
more?: MoreProps;
mobile: boolean;
tabBarGutter?: number;
renderTabBar?: RenderTabBar;
className?: string;
style?: React.CSSProperties;
locale?: TabsLocale;
onTabClick: (activeKey: string, e: React.MouseEvent | React.KeyboardEvent) => void;
onTabScroll?: OnTabScroll;
children?: (node: React.ReactElement) => React.ReactElement;
getPopupContainer?: (node: HTMLElement) => HTMLElement;
popupClassName?: string;
indicator?: {
size?: GetIndicatorSize;
align?: 'start' | 'center' | 'end';
};
classNames?: Partial<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
}
declare const TabNavList: React.ForwardRefExoticComponent<TabNavListProps & React.RefAttributes<HTMLDivElement>>;
export default TabNavList;
+589
View File
@@ -0,0 +1,589 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import { clsx } from 'clsx';
import ResizeObserver from '@rc-component/resize-observer';
import useEvent from "@rc-component/util/es/hooks/useEvent";
import { useComposeRef } from "@rc-component/util/es/ref";
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
import TabContext from "../TabContext";
import useIndicator from "../hooks/useIndicator";
import useOffsets from "../hooks/useOffsets";
import useSyncState from "../hooks/useSyncState";
import useTouchMove from "../hooks/useTouchMove";
import useUpdate, { useUpdateState } from "../hooks/useUpdate";
import useVisibleRange from "../hooks/useVisibleRange";
import { genDataNodeKey, getRemovable, stringify } from "../util";
import AddButton from "./AddButton";
import ExtraContent from "./ExtraContent";
import OperationNode from "./OperationNode";
import TabNode from "./TabNode";
const getTabSize = (tab, containerRect) => {
// tabListRef
const {
offsetWidth,
offsetHeight,
offsetTop,
offsetLeft
} = tab;
const {
width,
height,
left,
top
} = tab.getBoundingClientRect();
// Use getBoundingClientRect to avoid decimal inaccuracy
if (Math.abs(width - offsetWidth) < 1) {
return [width, height, left - containerRect.left, top - containerRect.top];
}
return [offsetWidth, offsetHeight, offsetLeft, offsetTop];
};
const getSize = refObj => {
const {
offsetWidth = 0,
offsetHeight = 0
} = refObj.current || {};
// Use getBoundingClientRect to avoid decimal inaccuracy
if (refObj.current) {
const {
width,
height
} = refObj.current.getBoundingClientRect();
if (Math.abs(width - offsetWidth) < 1) {
return [width, height];
}
}
return [offsetWidth, offsetHeight];
};
/**
* Convert `SizeInfo` to unit value. Such as [123, 456] with `top` position get `123`
*/
const getUnitValue = (size, tabPositionTopOrBottom) => {
return size[tabPositionTopOrBottom ? 0 : 1];
};
const TabNavList = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
className,
style,
id,
animated,
activeKey,
rtl,
extra,
editable,
locale,
tabPosition,
tabBarGutter,
children,
onTabClick,
onTabScroll,
indicator,
classNames: tabsClassNames,
styles
} = props;
const {
prefixCls,
tabs
} = React.useContext(TabContext);
const containerRef = useRef(null);
const extraLeftRef = useRef(null);
const extraRightRef = useRef(null);
const tabsWrapperRef = useRef(null);
const tabListRef = useRef(null);
const operationsRef = useRef(null);
const innerAddButtonRef = useRef(null);
const tabPositionTopOrBottom = tabPosition === 'top' || tabPosition === 'bottom';
const [transformLeft, setTransformLeft] = useSyncState(0, (next, prev) => {
if (tabPositionTopOrBottom && onTabScroll) {
onTabScroll({
direction: next > prev ? 'left' : 'right'
});
}
});
const [transformTop, setTransformTop] = useSyncState(0, (next, prev) => {
if (!tabPositionTopOrBottom && onTabScroll) {
onTabScroll({
direction: next > prev ? 'top' : 'bottom'
});
}
});
const [containerExcludeExtraSize, setContainerExcludeExtraSize] = useState([0, 0]);
const [tabContentSize, setTabContentSize] = useState([0, 0]);
const [addSize, setAddSize] = useState([0, 0]);
const [operationSize, setOperationSize] = useState([0, 0]);
const [tabSizes, setTabSizes] = useUpdateState(new Map());
const tabOffsets = useOffsets(tabs, tabSizes, tabContentSize[0]);
// ========================== Unit =========================
const containerExcludeExtraSizeValue = getUnitValue(containerExcludeExtraSize, tabPositionTopOrBottom);
const tabContentSizeValue = getUnitValue(tabContentSize, tabPositionTopOrBottom);
const addSizeValue = getUnitValue(addSize, tabPositionTopOrBottom);
const operationSizeValue = getUnitValue(operationSize, tabPositionTopOrBottom);
const needScroll = Math.floor(containerExcludeExtraSizeValue) < Math.floor(tabContentSizeValue + addSizeValue);
const visibleTabContentValue = needScroll ? containerExcludeExtraSizeValue - operationSizeValue : containerExcludeExtraSizeValue - addSizeValue;
// ========================== Util =========================
const operationsHiddenClassName = `${prefixCls}-nav-operations-hidden`;
let transformMin = 0;
let transformMax = 0;
if (!tabPositionTopOrBottom) {
transformMin = Math.min(0, visibleTabContentValue - tabContentSizeValue);
transformMax = 0;
} else if (rtl) {
transformMin = 0;
transformMax = Math.max(0, tabContentSizeValue - visibleTabContentValue);
} else {
transformMin = Math.min(0, visibleTabContentValue - tabContentSizeValue);
transformMax = 0;
}
function alignInRange(value) {
if (value < transformMin) {
return transformMin;
}
if (value > transformMax) {
return transformMax;
}
return value;
}
// ========================= Mobile ========================
const touchMovingRef = useRef(null);
const [lockAnimation, setLockAnimation] = useState();
function doLockAnimation() {
setLockAnimation(Date.now());
}
function clearTouchMoving() {
if (touchMovingRef.current) {
clearTimeout(touchMovingRef.current);
}
}
useTouchMove(tabsWrapperRef, (offsetX, offsetY) => {
function doMove(setState, offset) {
setState(value => {
const newValue = alignInRange(value + offset);
return newValue;
});
}
// Skip scroll if place is enough
if (!needScroll) {
return false;
}
if (tabPositionTopOrBottom) {
doMove(setTransformLeft, offsetX);
} else {
doMove(setTransformTop, offsetY);
}
clearTouchMoving();
doLockAnimation();
return true;
});
useEffect(() => {
clearTouchMoving();
if (lockAnimation) {
touchMovingRef.current = setTimeout(() => {
setLockAnimation(0);
}, 100);
}
return clearTouchMoving;
}, [lockAnimation]);
// ===================== Visible Range =====================
// Render tab node & collect tab offset
const [visibleStart, visibleEnd] = useVisibleRange(tabOffsets,
// Container
visibleTabContentValue,
// Transform
tabPositionTopOrBottom ? transformLeft : transformTop,
// Tabs
tabContentSizeValue,
// Add
addSizeValue,
// Operation
operationSizeValue, {
...props,
tabs
});
// ========================= Scroll ========================
const scrollToTab = useEvent((key = activeKey) => {
const tabOffset = tabOffsets.get(key) || {
width: 0,
height: 0,
left: 0,
right: 0,
top: 0
};
if (tabPositionTopOrBottom) {
// ============ Align with top & bottom ============
let newTransform = transformLeft;
// RTL
if (rtl) {
if (tabOffset.right < transformLeft) {
newTransform = tabOffset.right;
} else if (tabOffset.right + tabOffset.width > transformLeft + visibleTabContentValue) {
newTransform = tabOffset.right + tabOffset.width - visibleTabContentValue;
}
}
// LTR
else if (tabOffset.left < -transformLeft) {
newTransform = -tabOffset.left;
} else if (tabOffset.left + tabOffset.width > -transformLeft + visibleTabContentValue) {
newTransform = -(tabOffset.left + tabOffset.width - visibleTabContentValue);
}
setTransformTop(0);
setTransformLeft(alignInRange(newTransform));
} else {
// ============ Align with left & right ============
let newTransform = transformTop;
if (tabOffset.top < -transformTop) {
newTransform = -tabOffset.top;
} else if (tabOffset.top + tabOffset.height > -transformTop + visibleTabContentValue) {
newTransform = -(tabOffset.top + tabOffset.height - visibleTabContentValue);
}
setTransformLeft(0);
setTransformTop(alignInRange(newTransform));
}
});
// ========================= Focus =========================
const [focusKey, setFocusKey] = useState();
const [isMouse, setIsMouse] = useState(false);
const enabledTabs = tabs.filter(tab => !tab.disabled).map(tab => tab.key);
const onOffset = offset => {
const currentIndex = enabledTabs.indexOf(focusKey || activeKey);
const len = enabledTabs.length;
const nextIndex = (currentIndex + offset + len) % len;
const newKey = enabledTabs[nextIndex];
setFocusKey(newKey);
};
const handleRemoveTab = (removalTabKey, e) => {
const removeIndex = enabledTabs.indexOf(removalTabKey);
const removeTab = tabs.find(tab => tab.key === removalTabKey);
const removable = getRemovable(removeTab?.closable, removeTab?.closeIcon, editable, removeTab?.disabled);
if (removable) {
e.preventDefault();
e.stopPropagation();
editable.onEdit('remove', {
key: removalTabKey,
event: e
});
// when remove last tab, focus previous tab
if (removeIndex === enabledTabs.length - 1) {
onOffset(-1);
} else {
onOffset(1);
}
}
};
const handleMouseDown = (key, e) => {
setIsMouse(true);
// Middle mouse button
if (e.button === 1) {
handleRemoveTab(key, e);
}
};
const handleKeyDown = e => {
const {
code
} = e;
const isRTL = rtl && tabPositionTopOrBottom;
const firstEnabledTab = enabledTabs[0];
const lastEnabledTab = enabledTabs[enabledTabs.length - 1];
switch (code) {
// LEFT
case 'ArrowLeft':
{
if (tabPositionTopOrBottom) {
onOffset(isRTL ? 1 : -1);
}
break;
}
// RIGHT
case 'ArrowRight':
{
if (tabPositionTopOrBottom) {
onOffset(isRTL ? -1 : 1);
}
break;
}
// UP
case 'ArrowUp':
{
e.preventDefault();
if (!tabPositionTopOrBottom) {
onOffset(-1);
}
break;
}
// DOWN
case 'ArrowDown':
{
e.preventDefault();
if (!tabPositionTopOrBottom) {
onOffset(1);
}
break;
}
// HOME
case 'Home':
{
e.preventDefault();
setFocusKey(firstEnabledTab);
break;
}
// END
case 'End':
{
e.preventDefault();
setFocusKey(lastEnabledTab);
break;
}
// Enter & Space
case 'Enter':
case 'Space':
{
e.preventDefault();
onTabClick(focusKey ?? activeKey, e);
break;
}
// Backspace
case 'Backspace':
case 'Delete':
{
handleRemoveTab(focusKey, e);
break;
}
}
};
// ========================== Tab ==========================
const tabNodeStyle = {};
if (tabPositionTopOrBottom) {
tabNodeStyle.marginInlineStart = tabBarGutter;
} else {
tabNodeStyle.marginTop = tabBarGutter;
}
const tabNodes = tabs.map((tab, i) => {
const {
key
} = tab;
return /*#__PURE__*/React.createElement(TabNode, {
id: id,
prefixCls: prefixCls,
key: key,
tab: tab,
className: tabsClassNames?.item
/* first node should not have margin left */,
style: i === 0 ? styles?.item : {
...tabNodeStyle,
...styles?.item
},
closable: tab.closable,
editable: editable,
active: key === activeKey,
focus: key === focusKey,
renderWrapper: children,
removeAriaLabel: locale?.removeAriaLabel,
tabCount: enabledTabs.length,
currentPosition: i + 1,
onClick: e => {
onTabClick(key, e);
},
onKeyDown: handleKeyDown,
onFocus: () => {
if (!isMouse) {
setFocusKey(key);
}
scrollToTab(key);
doLockAnimation();
if (!tabsWrapperRef.current) {
return;
}
// Focus element will make scrollLeft change which we should reset back
if (!rtl) {
tabsWrapperRef.current.scrollLeft = 0;
}
tabsWrapperRef.current.scrollTop = 0;
},
onBlur: () => {
setFocusKey(undefined);
},
onMouseDown: e => handleMouseDown(key, e),
onMouseUp: () => {
setIsMouse(false);
}
});
});
// Update buttons records
const updateTabSizes = () => setTabSizes(() => {
const newSizes = new Map();
const listRect = tabListRef.current?.getBoundingClientRect();
tabs.forEach(({
key
}) => {
const btnNode = tabListRef.current?.querySelector(`[data-node-key="${genDataNodeKey(key)}"]`);
if (btnNode) {
const [width, height, left, top] = getTabSize(btnNode, listRect);
newSizes.set(key, {
width,
height,
left,
top
});
}
});
return newSizes;
});
useEffect(() => {
updateTabSizes();
}, [tabs.map(tab => tab.key).join('_')]);
const onListHolderResize = useUpdate(() => {
// Update wrapper records
const containerSize = getSize(containerRef);
const extraLeftSize = getSize(extraLeftRef);
const extraRightSize = getSize(extraRightRef);
setContainerExcludeExtraSize([containerSize[0] - extraLeftSize[0] - extraRightSize[0], containerSize[1] - extraLeftSize[1] - extraRightSize[1]]);
const newAddSize = getSize(innerAddButtonRef);
setAddSize(newAddSize);
const newOperationSize = getSize(operationsRef);
setOperationSize(newOperationSize);
// Which includes add button size
const tabContentFullSize = getSize(tabListRef);
setTabContentSize([tabContentFullSize[0] - newAddSize[0], tabContentFullSize[1] - newAddSize[1]]);
// Update buttons records
updateTabSizes();
});
// ======================== Dropdown =======================
const startHiddenTabs = tabs.slice(0, visibleStart);
const endHiddenTabs = tabs.slice(visibleEnd + 1);
const hiddenTabs = [...startHiddenTabs, ...endHiddenTabs];
// =================== Link & Operations ===================
const activeTabOffset = tabOffsets.get(activeKey);
const {
style: indicatorStyle
} = useIndicator({
activeTabOffset,
horizontal: tabPositionTopOrBottom,
indicator,
rtl
});
// ========================= Effect ========================
useEffect(() => {
scrollToTab();
}, [activeKey, transformMin, transformMax, stringify(activeTabOffset), stringify(tabOffsets), tabPositionTopOrBottom]);
// Should recalculate when rtl changed
useEffect(() => {
onListHolderResize();
// eslint-disable-next-line
}, [rtl]);
// ========================= Render ========================
const hasDropdown = !!hiddenTabs.length;
const wrapPrefix = `${prefixCls}-nav-wrap`;
let pingLeft;
let pingRight;
let pingTop;
let pingBottom;
if (tabPositionTopOrBottom) {
if (rtl) {
pingRight = transformLeft > 0;
pingLeft = transformLeft !== transformMax;
} else {
pingLeft = transformLeft < 0;
pingRight = transformLeft !== transformMin;
}
} else {
pingTop = transformTop < 0;
pingBottom = transformTop !== transformMin;
}
return /*#__PURE__*/React.createElement(ResizeObserver, {
onResize: onListHolderResize
}, /*#__PURE__*/React.createElement("div", {
ref: useComposeRef(ref, containerRef),
role: "tablist",
"aria-orientation": tabPositionTopOrBottom ? 'horizontal' : 'vertical',
className: clsx(`${prefixCls}-nav`, className, tabsClassNames?.header),
style: {
...styles?.header,
...style
},
onKeyDown: () => {
// No need animation when use keyboard
doLockAnimation();
}
}, /*#__PURE__*/React.createElement(ExtraContent, {
ref: extraLeftRef,
position: "left",
extra: extra,
prefixCls: prefixCls
}), /*#__PURE__*/React.createElement(ResizeObserver, {
onResize: onListHolderResize
}, /*#__PURE__*/React.createElement("div", {
className: clsx(wrapPrefix, {
[`${wrapPrefix}-ping-left`]: pingLeft,
[`${wrapPrefix}-ping-right`]: pingRight,
[`${wrapPrefix}-ping-top`]: pingTop,
[`${wrapPrefix}-ping-bottom`]: pingBottom
}),
ref: tabsWrapperRef
}, /*#__PURE__*/React.createElement(ResizeObserver, {
onResize: onListHolderResize
}, /*#__PURE__*/React.createElement("div", {
ref: tabListRef,
className: `${prefixCls}-nav-list`,
style: {
transform: `translate(${transformLeft}px, ${transformTop}px)`,
transition: lockAnimation ? 'none' : undefined
}
}, tabNodes, /*#__PURE__*/React.createElement(AddButton, {
ref: innerAddButtonRef,
prefixCls: prefixCls,
locale: locale,
editable: editable,
style: {
...(tabNodes.length === 0 ? undefined : tabNodeStyle),
visibility: hasDropdown ? 'hidden' : null
}
}), /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-ink-bar`, tabsClassNames?.indicator, {
[`${prefixCls}-ink-bar-animated`]: animated.inkBar
}),
style: {
...indicatorStyle,
...styles?.indicator
}
}))))), /*#__PURE__*/React.createElement(OperationNode, _extends({}, props, {
removeAriaLabel: locale?.removeAriaLabel,
ref: operationsRef,
prefixCls: prefixCls,
tabs: hiddenTabs,
className: !hasDropdown && operationsHiddenClassName,
popupStyle: styles?.popup,
tabMoving: !!lockAnimation
})), /*#__PURE__*/React.createElement(ExtraContent, {
ref: extraRightRef,
position: "right",
extra: extra,
prefixCls: prefixCls
})));
/* eslint-enable */
});
export default TabNavList;