1
This commit is contained in:
+10
@@ -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;
|
||||
+25
@@ -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;
|
||||
+9
@@ -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;
|
||||
+34
@@ -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;
|
||||
+24
@@ -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;
|
||||
+191
@@ -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);
|
||||
+27
@@ -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;
|
||||
+107
@@ -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;
|
||||
+5
@@ -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;
|
||||
+18
@@ -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;
|
||||
+33
@@ -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
@@ -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;
|
||||
Reference in New Issue
Block a user