refactor: tabs

refactor-tabs
tangjinzhou 2021-10-03 21:56:47 +08:00
parent 022a3ce795
commit 53625976b8
50 changed files with 2495 additions and 2188 deletions

View File

@ -2,7 +2,7 @@ import type { Ref } from 'vue';
import { ref } from 'vue';
export default function useState<T, R = Ref<T>>(
defaultStateValue: T | (() => T),
defaultStateValue?: T | (() => T),
): [R, (val: T) => void] {
const initValue: T =
typeof defaultStateValue === 'function' ? (defaultStateValue as any)() : defaultStateValue;

View File

@ -190,7 +190,8 @@ export { default as Tree, TreeNode, DirectoryTree } from './tree';
export type { TreeSelectProps } from './tree-select';
export { default as TreeSelect, TreeSelectNode } from './tree-select';
export { default as Tabs, TabPane, TabContent } from './tabs';
export type { TabsProps, TabPaneProps } from './tabs';
export { default as Tabs, TabPane } from './tabs';
export type { TagProps } from './tag';
export { default as Tag, CheckableTag } from './tag';

View File

@ -26,6 +26,7 @@ import EllipsisOutlined from '@ant-design/icons-vue/EllipsisOutlined';
import { cloneElement } from '../../_util/vnode';
export const menuProps = {
id: String,
prefixCls: String,
disabled: Boolean,
inlineCollapsed: Boolean,
@ -420,6 +421,7 @@ export default defineComponent({
itemComponent={MenuItem}
class={className.value}
role="menu"
id={props.id}
data={wrappedChildList}
renderRawItem={node => node}
renderRawRest={omitItems => {

View File

@ -14,6 +14,7 @@ import devWarning from '../../vc-util/devWarning';
let indexGuid = 0;
const menuItemProps = {
id: String,
role: String,
disabled: Boolean,
danger: Boolean,
@ -210,6 +211,7 @@ export default defineComponent({
<Overflow.Item
component="li"
{...attrs}
id={props.id}
style={{ ...((attrs.style as any) || {}), ...directionStyle.value }}
class={[
classNames.value,

View File

@ -686,17 +686,24 @@
@tabs-card-head-background: @background-color-light;
@tabs-card-height: 40px;
@tabs-card-active-color: @primary-color;
@tabs-card-horizontal-padding: (
(@tabs-card-height - floor(@font-size-base * @line-height-base)) / 2
) - @border-width-base @padding-md;
@tabs-card-horizontal-padding-sm: 6px @padding-md;
@tabs-card-horizontal-padding-lg: 7px @padding-md 6px;
@tabs-title-font-size: @font-size-base;
@tabs-title-font-size-lg: @font-size-lg;
@tabs-title-font-size-sm: @font-size-base;
@tabs-ink-bar-color: @primary-color;
@tabs-bar-margin: 0 0 16px 0;
@tabs-horizontal-margin: 0 32px 0 0;
@tabs-horizontal-padding: 12px 16px;
@tabs-horizontal-padding-lg: 16px;
@tabs-horizontal-padding-sm: 8px 16px;
@tabs-vertical-padding: 8px 24px;
@tabs-vertical-margin: 0 0 16px 0;
@tabs-bar-margin: 0 0 @margin-md 0;
@tabs-horizontal-gutter: 32px;
@tabs-horizontal-margin: 0 0 0 @tabs-horizontal-gutter;
@tabs-horizontal-margin-rtl: 0 0 0 32px;
@tabs-horizontal-padding: @padding-sm 0;
@tabs-horizontal-padding-lg: @padding-md 0;
@tabs-horizontal-padding-sm: @padding-xs 0;
@tabs-vertical-padding: @padding-xs @padding-lg;
@tabs-vertical-margin: @margin-md 0 0 0;
@tabs-scrolling-size: 32px;
@tabs-highlight-color: @primary-color;
@tabs-hover-color: @primary-5;

View File

@ -1,94 +0,0 @@
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import { tuple } from '../_util/type';
import UpOutlined from '@ant-design/icons-vue/UpOutlined';
import DownOutlined from '@ant-design/icons-vue/DownOutlined';
import LeftOutlined from '@ant-design/icons-vue/LeftOutlined';
import RightOutlined from '@ant-design/icons-vue/RightOutlined';
import ScrollableInkTabBar from '../vc-tabs/src/ScrollableInkTabBar';
import PropTypes from '../_util/vue-types';
const TabBar = defineComponent({
name: 'TabBar',
inheritAttrs: false,
props: {
prefixCls: PropTypes.string,
centered: PropTypes.looseBool.def(false),
tabBarStyle: PropTypes.style,
tabBarExtraContent: PropTypes.VNodeChild,
type: PropTypes.oneOf(tuple('line', 'card', 'editable-card')),
tabPosition: PropTypes.oneOf(tuple('top', 'right', 'bottom', 'left')).def('top'),
tabBarPosition: PropTypes.oneOf(tuple('top', 'right', 'bottom', 'left')),
size: PropTypes.oneOf(tuple('default', 'small', 'large')),
animated: {
type: [Boolean, Object] as PropType<boolean | { inkBar: boolean; tabPane: boolean }>,
default: undefined,
},
renderTabBar: PropTypes.func,
panels: PropTypes.array.def([]),
activeKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
tabBarGutter: PropTypes.number,
},
render() {
const {
centered,
tabBarStyle,
animated = true,
renderTabBar,
tabBarExtraContent,
tabPosition,
prefixCls,
type = 'line',
size,
} = this.$props;
const inkBarAnimated = typeof animated === 'object' ? animated.inkBar : animated;
const isVertical = tabPosition === 'left' || tabPosition === 'right';
const prevIcon = (
<span class={`${prefixCls}-tab-prev-icon`}>
{isVertical ? (
<UpOutlined class={`${prefixCls}-tab-prev-icon-target`} />
) : (
<LeftOutlined class={`${prefixCls}-tab-prev-icon-target`} />
)}
</span>
);
const nextIcon = (
<span class={`${prefixCls}-tab-next-icon`}>
{isVertical ? (
<DownOutlined class={`${prefixCls}-tab-next-icon-target`} />
) : (
<RightOutlined class={`${prefixCls}-tab-next-icon-target`} />
)}
</span>
);
// Additional className for style usage
const cls = {
[this.$attrs.class as string]: this.$attrs.class,
[`${prefixCls}-centered-bar`]: centered,
[`${prefixCls}-${tabPosition}-bar`]: true,
[`${prefixCls}-${size}-bar`]: !!size,
[`${prefixCls}-card-bar`]: type && type.indexOf('card') >= 0,
};
const renderProps = {
...this.$props,
...this.$attrs,
children: null,
inkBarAnimated,
extraContent: tabBarExtraContent,
prevIcon,
nextIcon,
style: tabBarStyle,
class: cls,
};
if (renderTabBar) {
return renderTabBar({ ...renderProps, DefaultTabBar: ScrollableInkTabBar });
} else {
return <ScrollableInkTabBar {...renderProps} />;
}
},
});
export default TabBar;

View File

@ -1,23 +1,19 @@
import type { App, Plugin } from 'vue';
import Tabs from './tabs';
import TabPane from '../vc-tabs/src/TabPane';
import TabContent from '../vc-tabs/src/TabContent';
import Tabs, { TabPane } from './src';
export type { TabsProps, TabPaneProps } from './src';
Tabs.TabPane = { ...TabPane, name: 'ATabPane', __ANT_TAB_PANE: true };
Tabs.TabContent = { ...TabContent, name: 'ATabContent' };
/* istanbul ignore next */
Tabs.install = function (app: App) {
app.component(Tabs.name, Tabs);
app.component(Tabs.TabPane.name, Tabs.TabPane);
app.component(Tabs.TabContent.name, Tabs.TabContent);
return app;
};
export default Tabs as typeof Tabs &
Plugin & {
readonly TabPane: typeof TabPane;
readonly TabContent: typeof TabContent;
};
export { TabPane, TabContent };
export { TabPane };

View File

@ -0,0 +1,33 @@
import type { Tab } from './interface';
import type { PropType, InjectionKey } from 'vue';
import { provide, inject, defineComponent } from 'vue';
export interface TabContextProps {
tabs: Tab[];
prefixCls: string;
}
const TabsContextKey: InjectionKey<TabContextProps> = Symbol('tabsContextKey');
export const useProvideTabs = (props: TabContextProps) => {
provide(TabsContextKey, props);
};
export const useInjectTabs = () => {
return inject(TabsContextKey, { tabs: [], prefixCls: undefined });
};
const TabsContextProvider = defineComponent({
name: 'TabsContextProvider',
inheritAttrs: false,
props: {
tabs: { type: Object as PropType<TabContextProps['tabs']>, default: undefined },
prefixCls: { type: String, default: undefined },
},
setup(props, { slots }) {
useProvideTabs(props);
return () => slots.default?.();
},
});
export default TabsContextProvider;

View File

@ -0,0 +1,48 @@
import type { PropType } from 'vue';
import { defineComponent, ref } from 'vue';
import type { EditableConfig, TabsLocale } from '../interface';
export interface AddButtonProps {
prefixCls: string;
editable?: EditableConfig;
locale?: TabsLocale;
}
export default defineComponent({
name: 'AddButton',
inheritAttrs: false,
props: {
prefixCls: String,
editable: { type: Object as PropType<EditableConfig> },
locale: { type: Object as PropType<TabsLocale>, default: undefined as TabsLocale },
},
setup(props, { expose, attrs }) {
const domRef = ref();
expose({
domRef,
});
return () => {
const { prefixCls, editable, locale } = props;
if (!editable || editable.showAdd === false) {
return null;
}
return (
<button
ref={domRef}
type="button"
class={`${prefixCls}-nav-add`}
style={attrs.style}
aria-label={locale?.addAriaLabel || 'Add tab'}
onClick={event => {
editable.onEdit('add', {
event,
});
}}
>
{editable.addIcon ? editable.addIcon() : '+'}
</button>
);
};
},
});

View File

@ -0,0 +1,217 @@
import Menu, { MenuItem } from '../../../menu';
import Dropdown from '../../../vc-dropdown';
import type { Tab, TabsLocale, EditableConfig } from '../interface';
import AddButton from './AddButton';
import type { Key, VueNode } from '../../../_util/type';
import KeyCode from '../../../_util/KeyCode';
import type { CSSProperties, PropType } from 'vue';
import classNames from '../../../_util/classNames';
import { defineComponent, watch, computed, onMounted } from 'vue';
import PropTypes from '../../../_util/vue-types';
import useState from '../../../_util/hooks/useState';
export interface OperationNodeProps {
prefixCls: string;
id: string;
tabs: Tab[];
rtl: boolean;
tabBarGutter?: number;
activeKey: string;
mobile: boolean;
moreIcon?: VueNode;
moreTransitionName?: string;
editable?: EditableConfig;
locale?: TabsLocale;
onTabClick: (key: Key, e: MouseEvent | KeyboardEvent) => void;
}
export default defineComponent({
name: 'OperationNode',
inheritAttrs: false,
props: {
prefixCls: { type: String },
id: { type: String },
tabs: { type: Object as PropType<Tab[]> },
rtl: { type: Boolean },
tabBarGutter: { type: Number },
activeKey: { type: String },
mobile: { type: Boolean },
moreIcon: PropTypes.any,
moreTransitionName: { type: String },
editable: { type: Object as PropType<EditableConfig> },
locale: { type: Object as PropType<TabsLocale>, default: undefined as TabsLocale },
onTabClick: { type: Function as PropType<(key: Key, e: MouseEvent | KeyboardEvent) => void> },
},
emits: ['tabClick'],
slots: ['moreIcon'],
setup(props, { attrs, slots }) {
// ======================== Dropdown ========================
const [open, setOpen] = useState(false);
const [selectedKey, setSelectedKey] = useState<string>(null);
const selectOffset = (offset: -1 | 1) => {
const enabledTabs = props.tabs.filter(tab => !tab.disabled);
let selectedIndex = enabledTabs.findIndex(tab => tab.key === selectedKey.value) || 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;
}
}
};
const onKeyDown = (e: KeyboardEvent) => {
const { which } = e;
if (!open.value) {
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.value !== null) props.onTabClick(selectedKey.value, e);
break;
}
};
const popupId = computed(() => `${props.id}-more-popup`);
const selectedItemId = computed(() =>
selectedKey.value !== null ? `${popupId.value}-${selectedKey.value}` : null,
);
onMounted(() => {
watch(
selectedKey,
() => {
const ele = document.getElementById(selectedItemId.value);
if (ele && ele.scrollIntoView) {
ele.scrollIntoView(false);
}
},
{ flush: 'post', immediate: true },
);
});
watch(open, () => {
if (!open.value) {
setSelectedKey(null);
}
});
return () => {
const {
prefixCls,
id,
tabs,
locale,
mobile,
moreIcon = slots.moreIcon?.() || 'More',
moreTransitionName,
editable,
tabBarGutter,
rtl,
onTabClick,
} = props;
const dropdownPrefix = `${prefixCls}-dropdown`;
const dropdownAriaLabel = locale?.dropdownAriaLabel;
// ========================= Render =========================
const moreStyle: CSSProperties = {
[rtl ? 'marginRight' : 'marginLeft']: tabBarGutter,
};
if (!tabs.length) {
moreStyle.visibility = 'hidden';
moreStyle.order = 1;
}
const overlayClassName = classNames({
[`${dropdownPrefix}-rtl`]: rtl,
});
const moreNode = mobile ? null : (
<Dropdown
prefixCls={dropdownPrefix}
trigger={['hover']}
visible={open.value}
transitionName={moreTransitionName}
onVisibleChange={setOpen}
overlayClassName={overlayClassName}
mouseEnterDelay={0.1}
mouseLeaveDelay={0.1}
v-slots={{
overlay: () => (
<Menu
onClick={({ key, domEvent }) => {
onTabClick(key, domEvent);
setOpen(false);
}}
id={popupId.value}
tabindex={-1}
role="listbox"
aria-activedescendant={selectedItemId.value}
selectedKeys={[selectedKey.value]}
aria-label={
dropdownAriaLabel !== undefined ? dropdownAriaLabel : 'expanded dropdown'
}
>
{tabs.map(tab => (
<MenuItem
key={tab.key}
id={`${popupId.value}-${tab.key}`}
role="option"
aria-controls={id && `${id}-panel-${tab.key}`}
disabled={tab.disabled}
>
{typeof tab.tab === 'function' ? tab.tab() : tab.tab}
</MenuItem>
))}
</Menu>
),
default: () => (
<button
type="button"
class={`${prefixCls}-nav-more`}
style={moreStyle}
tabindex={-1}
aria-hidden="true"
aria-haspopup="listbox"
aria-controls={popupId.value}
id={`${id}-more`}
aria-expanded={open.value}
onKeydown={onKeyDown}
>
{moreIcon}
</button>
),
}}
></Dropdown>
);
return (
<div class={classNames(`${prefixCls}-nav-operations`, attrs.class)} style={attrs.style}>
{moreNode}
<AddButton prefixCls={prefixCls} locale={locale} editable={editable} />
</div>
);
};
},
});

View File

@ -0,0 +1,138 @@
import type { Tab, EditableConfig } from '../interface';
import type { PropType } from 'vue';
import { onBeforeUnmount, defineComponent, computed, ref } from 'vue';
import type { FocusEventHandler } from '../../../_util/EventInterface';
import KeyCode from '../../../_util/KeyCode';
import classNames from '../../../_util/classNames';
export interface TabNodeProps {
id: string;
prefixCls: string;
tab: Tab;
active: boolean;
closable?: boolean;
editable?: EditableConfig;
onClick?: (e: MouseEvent | KeyboardEvent) => void;
onResize?: (width: number, height: number, left: number, top: number) => void;
renderWrapper?: (node: any) => any;
removeAriaLabel?: string;
onRemove: () => void;
onFocus: FocusEventHandler;
}
export default defineComponent({
name: 'TabNode',
props: {
id: { type: String as PropType<string> },
prefixCls: { type: String as PropType<string> },
tab: { type: Object as PropType<Tab> },
active: { type: Boolean },
closable: { type: Boolean },
editable: { type: Object as PropType<EditableConfig> },
onClick: { type: Function as PropType<(e: MouseEvent | KeyboardEvent) => void> },
onResize: {
type: Function as PropType<
(width: number, height: number, left: number, top: number) => void
>,
},
renderWrapper: { type: Function as PropType<(node: any) => any> },
removeAriaLabel: { type: String },
onRemove: { type: Function as PropType<() => void> },
onFocus: { type: Function as PropType<FocusEventHandler> },
},
emits: ['click', 'resize', 'remove', 'focus'],
setup(props, { expose, attrs }) {
const domRef = ref();
function onInternalClick(e: MouseEvent | KeyboardEvent) {
if (props.tab?.disabled) {
return;
}
props.onClick(e);
}
expose({
domRef,
});
onBeforeUnmount(() => {
props.onRemove();
});
function onRemoveTab(event: MouseEvent | KeyboardEvent) {
event.preventDefault();
event.stopPropagation();
props.editable.onEdit('remove', {
key: props.tab?.key,
event,
});
}
const removable = computed(
() => props.editable && props.closable !== false && !props.tab?.disabled,
);
return () => {
const {
prefixCls,
id,
active,
tab: { key, tab, disabled, closeIcon },
renderWrapper,
removeAriaLabel,
editable,
onFocus,
} = props;
const tabPrefix = `${prefixCls}-tab`;
const node = (
<div
key={key}
ref={domRef}
class={classNames(tabPrefix, {
[`${tabPrefix}-with-remove`]: removable.value,
[`${tabPrefix}-active`]: active,
[`${tabPrefix}-disabled`]: disabled,
})}
style={attrs.style}
onClick={onInternalClick}
>
{/* Primary Tab Button */}
<div
role="tab"
aria-selected={active}
id={id && `${id}-tab-${key}`}
class={`${tabPrefix}-btn`}
aria-controls={id && `${id}-panel-${key}`}
aria-disabled={disabled}
tabindex={disabled ? null : 0}
onClick={e => {
e.stopPropagation();
onInternalClick(e);
}}
onKeydown={e => {
if ([KeyCode.SPACE, KeyCode.ENTER].includes(e.which)) {
e.preventDefault();
onInternalClick(e);
}
}}
onFocus={onFocus}
>
{tab}
</div>
{/* Remove Button */}
{removable.value && (
<button
type="button"
aria-label={removeAriaLabel || 'remove'}
tabindex={0}
class={`${tabPrefix}-remove`}
onClick={e => {
e.stopPropagation();
onRemoveTab(e);
}}
>
{closeIcon || editable.removeIcon || '×'}
</button>
)}
</div>
);
return renderWrapper ? renderWrapper(node) : node;
};
},
});

View File

@ -0,0 +1,559 @@
import { useRafState } from '../hooks/useRaf';
import TabNode from './TabNode';
import type {
TabSizeMap,
TabPosition,
RenderTabBar,
TabsLocale,
EditableConfig,
AnimatedConfig,
OnTabScroll,
TabBarExtraPosition,
TabBarExtraContent,
} from '../interface';
import useOffsets from '../hooks/useOffsets';
import OperationNode from './OperationNode';
import { useInjectTabs } from '../TabContext';
import useTouchMove from '../hooks/useTouchMove';
import useRefs from '../hooks/useRefs';
import AddButton from './AddButton';
import type { Key } from '../../../_util/type';
import type { ExtractPropTypes, PropType, CSSProperties } from 'vue';
import { onBeforeUnmount, defineComponent, ref, watch, watchEffect, computed } from 'vue';
import PropTypes from '../../../_util/vue-types';
import useSyncState from '../hooks/useSyncState';
import useState from '../../../_util/hooks/useState';
import wrapperRaf from '../../../_util/raf';
import classNames from '../../../_util/classNames';
import ResizeObserver from '../../../vc-resize-observer';
import { toPx } from '../../../_util/util';
const DEFAULT_SIZE = { width: 0, height: 0, left: 0, top: 0, right: 0 };
const tabNavListProps = () => {
return {
id: { type: String },
tabPosition: { type: String as PropType<TabPosition> },
activeKey: { type: String },
rtl: { type: Boolean },
panes: PropTypes.any,
animated: { type: Object as PropType<AnimatedConfig>, default: undefined as AnimatedConfig },
extra: PropTypes.any,
editable: { type: Object as PropType<EditableConfig> },
moreIcon: PropTypes.any,
moreTransitionName: { type: String },
mobile: { type: Boolean },
tabBarGutter: { type: Number },
renderTabBar: { type: Function as PropType<RenderTabBar> },
locale: { type: Object as PropType<TabsLocale>, default: undefined as TabsLocale },
onTabClick: {
type: Function as PropType<(activeKey: Key, e: MouseEvent | KeyboardEvent) => void>,
},
onTabScroll: { type: Function as PropType<OnTabScroll> },
};
};
export type TabNavListProps = Partial<ExtractPropTypes<ReturnType<typeof tabNavListProps>>>;
interface ExtraContentProps {
position: TabBarExtraPosition;
prefixCls: string;
extra?: TabBarExtraContent;
}
export default defineComponent({
name: 'TabNavList',
inheritAttrs: false,
props: tabNavListProps(),
slots: ['panes', 'moreIcon', 'extra'],
emits: ['tabClick', 'tabScroll'],
setup(props, { attrs, slots }) {
const tabsContext = useInjectTabs();
const tabsWrapperRef = ref<HTMLDivElement>();
const tabListRef = ref<HTMLDivElement>();
const operationsRef = ref<{ $el: HTMLDivElement }>();
const innerAddButtonRef = ref<HTMLButtonElement>();
const [getBtnRef, removeBtnRef] = useRefs();
const tabPositionTopOrBottom = computed(
() => props.tabPosition === 'top' || props.tabPosition === 'bottom',
);
const [transformLeft, setTransformLeft] = useSyncState(0, (next, prev) => {
if (tabPositionTopOrBottom.value && props.onTabScroll) {
props.onTabScroll({ direction: next > prev ? 'left' : 'right' });
}
});
const [transformTop, setTransformTop] = useSyncState(0, (next, prev) => {
if (!tabPositionTopOrBottom.value && props.onTabScroll) {
props.onTabScroll({ direction: next > prev ? 'top' : 'bottom' });
}
});
const [wrapperScrollWidth, setWrapperScrollWidth] = useState<number>(0);
const [wrapperScrollHeight, setWrapperScrollHeight] = useState<number>(0);
const [wrapperContentWidth, setWrapperContentWidth] = useState<number>(0);
const [wrapperContentHeight, setWrapperContentHeight] = useState<number>(0);
const [wrapperWidth, setWrapperWidth] = useState<number>(null);
const [wrapperHeight, setWrapperHeight] = useState<number>(null);
const [addWidth, setAddWidth] = useState<number>(0);
const [addHeight, setAddHeight] = useState<number>(0);
const [tabSizes, setTabSizes] = useRafState<TabSizeMap>(new Map());
const tabOffsets = useOffsets(
computed(() => tabsContext.tabs),
tabSizes,
);
// ========================== Util =========================
const operationsHiddenClassName = computed(
() => `${tabsContext.prefixCls}-nav-operations-hidden`,
);
const transformMin = ref(0);
const transformMax = ref(0);
watchEffect(() => {
if (!tabPositionTopOrBottom.value) {
transformMin.value = Math.min(0, wrapperHeight.value - wrapperScrollHeight.value);
transformMax.value = 0;
} else if (props.rtl) {
transformMin.value = 0;
transformMax.value = Math.max(0, wrapperScrollWidth.value - wrapperWidth.value);
} else {
transformMin.value = Math.min(0, wrapperWidth.value - wrapperScrollWidth.value);
transformMax.value = 0;
}
});
const alignInRange = (value: number): number => {
if (value < transformMin.value) {
return transformMin.value;
}
if (value > transformMax.value) {
return transformMax.value;
}
return value;
};
// ========================= Mobile ========================
const touchMovingRef = ref<number>();
const [lockAnimation, setLockAnimation] = useState<number>();
const doLockAnimation = () => {
setLockAnimation(Date.now());
};
const clearTouchMoving = () => {
window.clearTimeout(touchMovingRef.value);
};
const doMove = (setState: (fn: (val: number) => number) => void, offset: number) => {
setState((value: number) => {
const newValue = alignInRange(value + offset);
return newValue;
});
};
useTouchMove(tabsWrapperRef, (offsetX, offsetY) => {
if (tabPositionTopOrBottom.value) {
// Skip scroll if place is enough
if (wrapperWidth.value >= wrapperScrollWidth.value) {
return false;
}
doMove(setTransformLeft, offsetX);
} else {
if (wrapperHeight.value >= wrapperScrollHeight.value) {
return false;
}
doMove(setTransformTop, offsetY);
}
clearTouchMoving();
doLockAnimation();
return true;
});
watch(lockAnimation, () => {
clearTouchMoving();
if (lockAnimation.value) {
touchMovingRef.value = window.setTimeout(() => {
setLockAnimation(0);
}, 100);
}
});
// ========================= Scroll ========================
const scrollToTab = (key = props.activeKey) => {
const tabOffset = tabOffsets.value.get(key) || {
width: 0,
height: 0,
left: 0,
right: 0,
top: 0,
};
if (tabPositionTopOrBottom.value) {
// ============ Align with top & bottom ============
let newTransform = transformLeft.value;
// RTL
if (props.rtl) {
if (tabOffset.right < transformLeft.value) {
newTransform = tabOffset.right;
} else if (tabOffset.right + tabOffset.width > transformLeft.value + wrapperWidth.value) {
newTransform = tabOffset.right + tabOffset.width - wrapperWidth.value;
}
}
// LTR
else if (tabOffset.left < -transformLeft.value) {
newTransform = -tabOffset.left;
} else if (tabOffset.left + tabOffset.width > -transformLeft.value + wrapperWidth.value) {
newTransform = -(tabOffset.left + tabOffset.width - wrapperWidth.value);
}
setTransformTop(0);
setTransformLeft(alignInRange(newTransform));
} else {
// ============ Align with left & right ============
let newTransform = transformTop.value;
if (tabOffset.top < -transformTop.value) {
newTransform = -tabOffset.top;
} else if (tabOffset.top + tabOffset.height > -transformTop.value + wrapperHeight.value) {
newTransform = -(tabOffset.top + tabOffset.height - wrapperHeight.value);
}
setTransformLeft(0);
setTransformTop(alignInRange(newTransform));
}
};
const visibleStart = ref(0);
const visibleEnd = ref(0);
watchEffect(() => {
let unit: 'width' | 'height';
let position: 'left' | 'top' | 'right';
let transformSize: number;
let basicSize: number;
let tabContentSize: number;
let addSize: number;
if (['top', 'bottom'].includes(props.tabPosition)) {
unit = 'width';
basicSize = wrapperContentWidth.value;
tabContentSize = wrapperContentWidth.value;
addSize = addWidth.value;
position = props.rtl ? 'right' : 'left';
transformSize = Math.abs(transformLeft.value);
} else {
unit = 'height';
basicSize = wrapperContentHeight.value;
tabContentSize = wrapperContentHeight.value;
addSize = addHeight.value;
position = 'top';
transformSize = -transformTop.value;
}
let mergedBasicSize = basicSize;
if (tabContentSize + addSize > basicSize) {
mergedBasicSize = basicSize - addSize;
}
const { tabs } = tabsContext;
if (!tabs.length) {
[visibleStart.value, visibleEnd.value] = [0, 0];
}
const len = tabs.length;
let endIndex = len;
for (let i = 0; i < len; i += 1) {
const offset = tabOffsets.value.get(tabs[i].key) || DEFAULT_SIZE;
if (offset[position] + offset[unit] > transformSize + mergedBasicSize) {
endIndex = i - 1;
break;
}
}
let startIndex = 0;
for (let i = len - 1; i >= 0; i -= 1) {
const offset = tabOffsets.value.get(tabs[i].key) || DEFAULT_SIZE;
if (offset[position] < transformSize) {
startIndex = i + 1;
break;
}
}
[visibleStart.value, visibleEnd.value] = [startIndex, endIndex];
});
const onListHolderResize = () => {
// Update wrapper records
const offsetWidth = tabsWrapperRef.value?.offsetWidth || 0;
const offsetHeight = tabsWrapperRef.value?.offsetHeight || 0;
const newAddWidth = innerAddButtonRef.value?.offsetWidth || 0;
const newAddHeight = innerAddButtonRef.value?.offsetHeight || 0;
const newOperationWidth = operationsRef.value?.$el.offsetWidth || 0;
const newOperationHeight = operationsRef.value?.$el.offsetHeight || 0;
setWrapperWidth(offsetWidth);
setWrapperHeight(offsetHeight);
setAddWidth(newAddWidth);
setAddHeight(newAddHeight);
const newWrapperScrollWidth = (tabListRef.value?.offsetWidth || 0) - newAddWidth;
const newWrapperScrollHeight = (tabListRef.value?.offsetHeight || 0) - newAddHeight;
setWrapperScrollWidth(newWrapperScrollWidth);
setWrapperScrollHeight(newWrapperScrollHeight);
const isOperationHidden = operationsRef.value?.$el.className.includes(
operationsHiddenClassName.value,
);
setWrapperContentWidth(newWrapperScrollWidth - (isOperationHidden ? 0 : newOperationWidth));
setWrapperContentHeight(
newWrapperScrollHeight - (isOperationHidden ? 0 : newOperationHeight),
);
// Update buttons records
setTabSizes(() => {
const newSizes: TabSizeMap = new Map();
tabsContext.tabs.forEach(({ key }) => {
const btnRef = getBtnRef(key).value;
const btnNode = (btnRef as any).$el || btnRef;
if (btnNode) {
newSizes.set(key, {
width: btnNode.offsetWidth,
height: btnNode.offsetHeight,
left: btnNode.offsetLeft,
top: btnNode.offsetTop,
});
}
});
return newSizes;
});
};
// ======================== Dropdown =======================
const hiddenTabs = computed(() => [
...tabsContext.tabs.slice(0, visibleStart.value),
...tabsContext.tabs.slice(visibleEnd.value + 1),
]);
// =================== Link & Operations ===================
const [inkStyle, setInkStyle] = useState<CSSProperties>();
const activeTabOffset = computed(() => tabOffsets.value.get(props.activeKey));
// Delay set ink style to avoid remove tab blink
const inkBarRafRef = ref<number>();
const cleanInkBarRaf = () => {
wrapperRaf.cancel(inkBarRafRef.value);
};
watch([activeTabOffset, tabPositionTopOrBottom, () => props.rtl], () => {
const newInkStyle: CSSProperties = {};
if (activeTabOffset.value) {
if (tabPositionTopOrBottom.value) {
if (props.rtl) {
newInkStyle.right = toPx(activeTabOffset.value.right);
} else {
newInkStyle.left = toPx(activeTabOffset.value.left);
}
newInkStyle.width = toPx(activeTabOffset.value.width);
} else {
newInkStyle.top = toPx(activeTabOffset.value.top);
newInkStyle.height = toPx(activeTabOffset.value.height);
}
}
cleanInkBarRaf();
inkBarRafRef.value = wrapperRaf(() => {
setInkStyle(newInkStyle);
});
});
watch(
[() => props.activeKey, activeTabOffset, tabOffsets, tabPositionTopOrBottom],
() => {
scrollToTab();
},
{ flush: 'post' },
);
watch(
[() => props.rtl, () => props.tabBarGutter, () => props.activeKey, () => tabsContext.tabs],
() => {
onListHolderResize();
},
{ flush: 'post' },
);
const ExtraContent = ({ position, prefixCls, extra }: ExtraContentProps) => {
if (!extra) return null;
const content = slots.extra?.({ position });
return content ? <div class={`${prefixCls}-extra-content`}>{content}</div> : null;
};
onBeforeUnmount(() => {
clearTouchMoving();
cleanInkBarRaf();
});
return () => {
const { prefixCls, tabs } = tabsContext;
const {
id,
animated,
activeKey,
rtl,
extra,
editable,
locale,
tabPosition,
tabBarGutter,
onTabClick,
} = props;
const { class: className, style } = attrs;
// ========================= Render ========================
const hasDropdown = !!hiddenTabs.value.length;
const wrapPrefix = `${prefixCls}-nav-wrap`;
let pingLeft: boolean;
let pingRight: boolean;
let pingTop: boolean;
let pingBottom: boolean;
if (tabPositionTopOrBottom.value) {
if (rtl) {
pingRight = transformLeft.value > 0;
pingLeft = transformLeft.value + wrapperWidth.value < wrapperScrollWidth.value;
} else {
pingLeft = transformLeft.value < 0;
pingRight = -transformLeft.value + wrapperWidth.value < wrapperScrollWidth.value;
}
} else {
pingTop = transformTop.value < 0;
pingBottom = -transformTop.value + wrapperHeight.value < wrapperScrollHeight.value;
}
const tabNodeStyle: CSSProperties = {};
if (tabPosition === 'top' || tabPosition === 'bottom') {
tabNodeStyle[rtl ? 'marginRight' : 'marginLeft'] =
typeof tabBarGutter === 'number' ? `${tabBarGutter}px` : tabBarGutter;
} else {
tabNodeStyle.marginTop =
typeof tabBarGutter === 'number' ? `${tabBarGutter}px` : tabBarGutter;
}
const tabNodes = tabs.map((tab, i) => {
const { key } = tab;
return (
<TabNode
id={id}
prefixCls={prefixCls}
key={key}
tab={tab}
/* first node should not have margin left */
style={i === 0 ? undefined : tabNodeStyle}
closable={tab.closable}
editable={editable}
active={key === activeKey}
removeAriaLabel={locale?.removeAriaLabel}
ref={getBtnRef(key)}
onClick={e => {
onTabClick(key, e);
}}
onRemove={() => {
removeBtnRef(key);
}}
onFocus={() => {
scrollToTab(key);
doLockAnimation();
if (!tabsWrapperRef.value) {
return;
}
// Focus element will make scrollLeft change which we should reset back
if (!rtl) {
tabsWrapperRef.value.scrollLeft = 0;
}
tabsWrapperRef.value.scrollTop = 0;
}}
v-slots={slots}
></TabNode>
);
});
return (
<div
ref={ref}
role="tablist"
class={classNames(`${prefixCls}-nav`, className)}
style={style}
onKeydown={() => {
// No need animation when use keyboard
doLockAnimation();
}}
>
<ExtraContent position="left" extra={extra} prefixCls={prefixCls} />
<ResizeObserver onResize={onListHolderResize}>
<div
class={classNames(wrapPrefix, {
[`${wrapPrefix}-ping-left`]: pingLeft,
[`${wrapPrefix}-ping-right`]: pingRight,
[`${wrapPrefix}-ping-top`]: pingTop,
[`${wrapPrefix}-ping-bottom`]: pingBottom,
})}
ref={tabsWrapperRef}
>
<ResizeObserver onResize={onListHolderResize}>
<div
ref={tabListRef}
class={`${prefixCls}-nav-list`}
style={{
transform: `translate(${transformLeft.value}px, ${transformTop.value}px)`,
transition: lockAnimation.value ? 'none' : undefined,
}}
>
{tabNodes}
<AddButton
ref={innerAddButtonRef}
prefixCls={prefixCls}
locale={locale}
editable={editable}
style={{
...(tabNodes.length === 0 ? undefined : tabNodeStyle),
visibility: hasDropdown ? 'hidden' : null,
}}
/>
<div
class={classNames(`${prefixCls}-ink-bar`, {
[`${prefixCls}-ink-bar-animated`]: animated.inkBar,
})}
style={inkStyle.value}
/>
</div>
</ResizeObserver>
</div>
</ResizeObserver>
<OperationNode
{...props}
ref={operationsRef}
prefixCls={prefixCls}
tabs={hiddenTabs.value}
class={!hasDropdown && operationsHiddenClassName.value}
/>
<ExtraContent position="right" extra={extra} prefixCls={prefixCls} />
</div>
);
};
},
});

View File

@ -0,0 +1,85 @@
import { defineComponent, ref, watch, computed } from 'vue';
import type { CSSProperties } from 'vue';
import type { VueNode } from '../../../_util/type';
import PropTypes from '../../../_util/vue-types';
export interface TabPaneProps {
tab?: VueNode | (() => VueNode);
disabled?: boolean;
forceRender?: boolean;
closable?: boolean;
closeIcon?: VueNode;
// Pass by TabPaneList
prefixCls?: string;
tabKey?: string;
id?: string;
animated?: boolean;
active?: boolean;
destroyInactiveTabPane?: boolean;
}
export default defineComponent({
name: 'TabPane',
inheritAttrs: false,
props: {
tab: PropTypes.any,
disabled: { type: Boolean },
forceRender: { type: Boolean },
closable: { type: Boolean },
animated: { type: Boolean },
active: { type: Boolean },
destroyInactiveTabPane: { type: Boolean },
// Pass by TabPaneList
prefixCls: { type: String },
tabKey: { type: String },
id: { type: String },
},
slots: ['closeIcon', 'tab'],
setup(props, { attrs, slots }) {
const visited = ref(props.forceRender);
watch(
[() => props.active, () => props.destroyInactiveTabPane],
() => {
if (props.active) {
visited.value = true;
} else if (props.destroyInactiveTabPane) {
visited.value = false;
}
},
{ immediate: true },
);
const mergedStyle = computed<CSSProperties>(() => {
if (!props.active) {
if (props.animated) {
return {
visibility: 'hidden',
height: 0,
overflowY: 'hidden',
};
} else {
return { display: 'none' };
}
}
return {};
});
return () => {
const { prefixCls, forceRender, id, active, tabKey } = props;
return (
<div
id={id && `${id}-panel-${tabKey}`}
role="tabpanel"
tabindex={active ? 0 : -1}
aria-labelledby={id && `${id}-tab-${tabKey}`}
aria-hidden={!active}
style={{ ...mergedStyle.value, ...(attrs.style as any) }}
class={[`${prefixCls}-tabpane`, active && `${prefixCls}-tabpane-active`, attrs.class]}
>
{(active || visited.value || forceRender) && slots.default?.()}
</div>
);
};
},
});

View File

@ -0,0 +1,68 @@
import { useInjectTabs } from '../TabContext';
import type { TabPosition, AnimatedConfig } from '../interface';
import type { Key } from '../../../_util/type';
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import { cloneElement } from '../../../_util/vnode';
export interface TabPanelListProps {
activeKey: Key;
id: string;
rtl: boolean;
animated?: AnimatedConfig;
tabPosition?: TabPosition;
destroyInactiveTabPane?: boolean;
}
export default defineComponent({
name: 'TabPanelList',
inheritAttrs: false,
props: {
activeKey: { type: [String, Number] as PropType<Key> },
id: { type: String },
rtl: { type: Boolean },
animated: { type: Object as PropType<AnimatedConfig>, default: undefined as AnimatedConfig },
tabPosition: { type: String as PropType<TabPosition> },
destroyInactiveTabPane: { type: Boolean },
},
setup(props) {
const tabsContext = useInjectTabs();
return () => {
const { id, activeKey, animated, tabPosition, rtl, destroyInactiveTabPane } = props;
const { prefixCls, tabs } = tabsContext;
const tabPaneAnimated = animated.tabPane;
const activeIndex = tabs.findIndex(tab => tab.key === activeKey);
return (
<div class={`${prefixCls}-content-holder`}>
<div
class={[
`${prefixCls}-content`,
`${prefixCls}-content-${tabPosition}`,
{
[`${prefixCls}-content-animated`]: tabPaneAnimated,
},
]}
style={
activeIndex && tabPaneAnimated
? { [rtl ? 'marginRight' : 'marginLeft']: `-${activeIndex}00%` }
: null
}
>
{tabs.map(tab => {
return cloneElement(tab.node, {
key: tab.key,
prefixCls,
tabKey: tab.key,
id,
animated: tabPaneAnimated,
active: tab.key === activeKey,
destroyInactiveTabPane,
});
})}
</div>
</div>
);
};
},
});

View File

@ -0,0 +1,334 @@
// Accessibility https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Tab_Role
import TabNavList from './TabNavList';
import TabPanelList from './TabPanelList';
import type {
TabPosition,
RenderTabBar,
TabsLocale,
EditableConfig,
AnimatedConfig,
OnTabScroll,
Tab,
} from './interface';
import type { CSSProperties, PropType, ExtractPropTypes } from 'vue';
import { defineComponent, computed, onMounted, watchEffect, camelize } from 'vue';
import { flattenChildren, initDefaultProps, isValidElement } from '../../_util/props-util';
import useConfigInject from '../../_util/hooks/useConfigInject';
import useState from '../../_util/hooks/useState';
import isMobile from '../../vc-util/isMobile';
import useMergedState from '../../_util/hooks/useMergedState';
import classNames from '../../_util/classNames';
import { CloseOutlined, PlusOutlined } from '@ant-design/icons-vue';
import devWarning from '../../vc-util/devWarning';
import type { SizeType } from '../../config-provider';
import TabsContextProvider from './TabContext';
export type TabsType = 'line' | 'card' | 'editable-card';
export type TabsPosition = 'top' | 'right' | 'bottom' | 'left';
// Used for accessibility
let uuid = 0;
export const tabsProps = () => {
return {
prefixCls: { type: String },
id: { type: String },
activeKey: { type: String },
defaultActiveKey: { type: String },
direction: { type: String as PropType<'ltr' | 'rtl'> },
animated: { type: [Boolean, Object] as PropType<boolean | AnimatedConfig> },
renderTabBar: { type: Function as PropType<RenderTabBar> },
tabBarGutter: { type: Number },
tabBarStyle: { type: Object as PropType<CSSProperties> },
tabPosition: { type: String as PropType<TabPosition> },
destroyInactiveTabPane: { type: Boolean },
hideAdd: Boolean,
type: { type: String as PropType<TabsType> },
size: { type: String as PropType<SizeType> },
centered: Boolean,
onEdit: {
type: Function as PropType<
(e: MouseEvent | KeyboardEvent | string, action: 'add' | 'remove') => void
>,
},
onChange: { type: Function as PropType<(activeKey: string) => void> },
onTabClick: {
type: Function as PropType<(activeKey: string, e: KeyboardEvent | MouseEvent) => void>,
},
onTabScroll: { type: Function as PropType<OnTabScroll> },
// Accessibility
locale: { type: Object as PropType<TabsLocale>, default: undefined as TabsLocale },
onPrevClick: Function,
onNextClick: Function,
};
};
export type TabsProps = Partial<ExtractPropTypes<ReturnType<typeof tabsProps>>>;
function parseTabList(children: any[]): Tab[] {
return children
.map(node => {
if (isValidElement(node)) {
const props = { ...(node.props || {}) };
for (const [k, v] of Object.entries(props)) {
delete props[k];
props[camelize(k)] = v;
}
const slots = node.children || {};
const key = node.key !== undefined ? String(node.key) : undefined;
const {
tab = slots.tab,
disabled,
forceRender,
closable,
animated,
active,
destroyInactiveTabPane,
} = props;
return {
key,
...props,
node,
closeIcon: slots.closeIcon,
tab,
disabled: disabled === '' || disabled,
forceRender: forceRender === '' || forceRender,
closable: closable === '' || closable,
animated: animated === '' || animated,
active: active === '' || active,
destroyInactiveTabPane: destroyInactiveTabPane === '' || destroyInactiveTabPane,
};
}
return null;
})
.filter(tab => tab);
}
const InternalTabs = defineComponent({
name: 'InternalTabs',
inheritAttrs: false,
props: {
...initDefaultProps(tabsProps(), {
tabPosition: 'top',
animated: {
inkBar: true,
tabPane: false,
},
}),
tabs: { type: Array as PropType<Tab[]> },
},
slots: ['tabBarExtraContent', 'moreIcon', 'addIcon', 'removeIcon'],
emits: ['tabClick', 'tabScroll', 'change', 'update:activeKey'],
setup(props, { attrs, slots }) {
devWarning(
!(props.onPrevClick !== undefined) && !(props.onNextClick !== undefined),
'Tabs',
'`onPrevClick / @prevClick` and `onNextClick / @nextClick` has been removed. Please use `onTabScroll / @tabScroll` instead.',
);
const { prefixCls, direction, size, rootPrefixCls } = useConfigInject('tabs', props);
const rtl = computed(() => direction.value === 'rtl');
const mergedAnimated = computed<AnimatedConfig>(() => {
const { animated } = props;
if (animated === false) {
return {
inkBar: false,
tabPane: false,
};
} else if (animated === true) {
return {
inkBar: true,
tabPane: true,
};
} else {
return {
inkBar: true,
tabPane: false,
...(typeof animated === 'object' ? animated : {}),
};
}
});
// ======================== Mobile ========================
const [mobile, setMobile] = useState(false);
onMounted(() => {
// Only update on the client side
setMobile(isMobile());
});
// ====================== Active Key ======================
const [mergedActiveKey, setMergedActiveKey] = useMergedState<string>(() => props.tabs[0]?.key, {
value: computed(() => props.activeKey),
defaultValue: props.defaultActiveKey,
});
const [activeIndex, setActiveIndex] = useState(() =>
props.tabs.findIndex(tab => tab.key === mergedActiveKey.value),
);
watchEffect(() => {
let newActiveIndex = props.tabs.findIndex(tab => tab.key === mergedActiveKey.value);
if (newActiveIndex === -1) {
newActiveIndex = Math.max(0, Math.min(activeIndex.value, props.tabs.length - 1));
setMergedActiveKey(props.tabs[newActiveIndex]?.key);
}
setActiveIndex(newActiveIndex);
});
// ===================== Accessibility ====================
const [mergedId, setMergedId] = useMergedState(null, {
value: computed(() => props.id),
});
const mergedTabPosition = computed(() => {
if (mobile.value && !['left', 'right'].includes(props.tabPosition)) {
return 'top';
} else {
return props.tabPosition;
}
});
onMounted(() => {
if (!props.id) {
setMergedId(`rc-tabs-${process.env.NODE_ENV === 'test' ? 'test' : uuid}`);
uuid += 1;
}
});
// ======================== Events ========================
const onInternalTabClick = (key: string, e: MouseEvent | KeyboardEvent) => {
props.onTabClick?.(key, e);
setMergedActiveKey(key);
props.onChange?.(key);
};
return () => {
const {
id,
type,
activeKey,
defaultActiveKey,
tabBarGutter,
tabBarStyle,
locale,
destroyInactiveTabPane,
renderTabBar,
onChange,
onTabClick,
onTabScroll,
hideAdd,
centered,
...restProps
} = props;
// ======================== Render ========================
const sharedProps = {
id: mergedId.value,
activeKey: mergedActiveKey.value,
animated: mergedAnimated.value,
tabPosition: mergedTabPosition.value,
rtl: rtl.value,
mobile: mobile.value,
};
let editable: EditableConfig | undefined;
if (type === 'editable-card') {
editable = {
onEdit: (editType, { key, event }) => {
props.onEdit?.(editType === 'add' ? event : key!, editType);
},
removeIcon: () => <CloseOutlined />,
addIcon: slots.addIcon ? slots.addIcon : () => <PlusOutlined />,
showAdd: hideAdd !== true,
};
}
let tabNavBar;
const tabNavBarProps = {
...sharedProps,
moreTransitionName: `${rootPrefixCls.value}-slide-up`,
editable,
locale,
tabBarGutter,
onTabClick: onInternalTabClick,
onTabScroll,
style: tabBarStyle,
panes: flattenChildren(slots.default?.()),
};
if (renderTabBar) {
tabNavBar = renderTabBar(tabNavBarProps, TabNavList);
} else {
tabNavBar = (
<TabNavList
{...tabNavBarProps}
v-slots={{
moreIcon: slots.moreIcon,
extra: slots.tabBarExtraContent,
}}
/>
);
}
const pre = prefixCls.value;
return (
<TabsContextProvider tabs={props.tabs} prefixCls={pre}>
<div
{...attrs}
id={id}
class={classNames(
pre,
`${pre}-${mergedTabPosition.value}`,
{
[`${pre}-${size}`]: size.value,
[`${pre}-card`]: ['card', 'editable-card'].includes(type as string),
[`${pre}-editable-card`]: type === 'editable-card',
[`${pre}-centered`]: centered,
[`${pre}-mobile`]: mobile.value,
[`${pre}-editable`]: type === 'editable-card',
[`${pre}-rtl`]: rtl.value,
},
attrs.class,
)}
{...restProps}
>
{tabNavBar}
<TabPanelList
destroyInactiveTabPane={destroyInactiveTabPane}
{...sharedProps}
animated={mergedAnimated.value}
/>
</div>
</TabsContextProvider>
);
};
},
});
export default defineComponent({
name: 'ATabs',
inheritAttrs: false,
props: initDefaultProps(tabsProps(), {
tabPosition: 'top',
animated: {
inkBar: true,
tabPane: false,
},
}),
slots: ['tabBarExtraContent', 'moreIcon', 'addIcon', 'removeIcon'],
emits: ['tabClick', 'tabScroll', 'change', 'update:activeKey'],
setup(props, { attrs, slots, emit }) {
const handleChange = (key: string) => {
emit('update:activeKey', key);
emit('change', key);
};
return () => {
const tabs = parseTabList(flattenChildren(slots.default?.()));
return (
<InternalTabs {...props} {...attrs} onChange={handleChange} tabs={tabs} v-slots={slots} />
);
};
},
});

View File

@ -0,0 +1,40 @@
import type { Ref } from 'vue';
import { ref, watchEffect } from 'vue';
import type { TabSizeMap, TabOffsetMap, Tab, TabOffset } from '../interface';
const DEFAULT_SIZE = { width: 0, height: 0, left: 0, top: 0 };
export default function useOffsets(
tabs: Ref<Tab[]>,
tabSizes: Ref<TabSizeMap>,
// holderScrollWidth: Ref<number>,
): Ref<TabOffsetMap> {
const offsetMap = ref<TabOffsetMap>(new Map());
watchEffect(() => {
const map: TabOffsetMap = new Map();
const tabsValue = tabs.value;
const lastOffset = tabSizes.value.get(tabsValue[0]?.key) || DEFAULT_SIZE;
const rightOffset = lastOffset.left + lastOffset.width;
for (let i = 0; i < tabsValue.length; i += 1) {
const { key } = tabsValue[i];
let data = tabSizes.value.get(key);
// Reuse last one when not exist yet
if (!data) {
data = tabSizes.value.get(tabsValue[i - 1]?.key) || DEFAULT_SIZE;
}
const entity = (map.get(key) || { ...data }) as TabOffset;
// Right
entity.right = rightOffset - entity.left - entity.width;
// Update entity
map.set(key, entity);
}
offsetMap.value = new Map(map);
});
return offsetMap;
}

View File

@ -0,0 +1,52 @@
import type { Ref } from 'vue';
import { ref, onBeforeUnmount } from 'vue';
import wrapperRaf from '../../../_util/raf';
export default function useRaf<Callback extends Function>(callback: Callback) {
const rafRef = ref<number>();
const removedRef = ref(false);
function trigger(...args: any[]) {
if (!removedRef.value) {
wrapperRaf.cancel(rafRef.value);
rafRef.value = wrapperRaf(() => {
callback(...args);
});
}
}
onBeforeUnmount(() => {
removedRef.value = true;
wrapperRaf.cancel(rafRef.value);
});
return trigger;
}
type Callback<T> = (ori: T) => T;
export function useRafState<T>(
defaultState: T | (() => T),
): [Ref<T>, (updater: Callback<T>) => void] {
const batchRef = ref<Callback<T>[]>([]);
const state: Ref<T> = ref(
typeof defaultState === 'function' ? (defaultState as any)() : defaultState,
);
const flushUpdate = useRaf(() => {
let value = state.value;
batchRef.value.forEach(callback => {
value = callback(value);
});
batchRef.value = [];
state.value = value;
});
function updater(callback: Callback<T>) {
batchRef.value.push(callback);
flushUpdate();
}
return [state, updater];
}

View File

@ -0,0 +1,23 @@
import type { Ref, ComponentPublicInstance } from 'vue';
import { ref } from 'vue';
import type { Key } from '../../../_util/type';
export default function useRefs(): [
(key: Key) => Ref<HTMLElement | ComponentPublicInstance>,
(key: Key) => void,
] {
const cacheRefs = ref(new Map<Key, Ref<HTMLElement | ComponentPublicInstance>>());
function getRef(key: Key) {
if (!cacheRefs.value.has(key)) {
cacheRefs.value.set(key, ref());
}
return cacheRefs.value.get(key);
}
function removeRef(key: Key) {
cacheRefs.value.delete(key);
}
return [getRef, removeRef];
}

View File

@ -0,0 +1,21 @@
import type { Ref } from 'vue';
import { ref } from 'vue';
type Updater<T> = (prev: T) => T;
export default function useSyncState<T>(
defaultState: T,
onChange: (newValue: T, prevValue: T) => void,
): [Ref<T>, (updater: T | Updater<T>) => void] {
const stateRef = ref(defaultState);
function setState(updater: any) {
const newValue = typeof updater === 'function' ? updater(stateRef.value) : updater;
if (newValue !== stateRef.value) {
onChange(newValue, stateRef.value as T);
}
stateRef.value = newValue;
}
return [stateRef as Ref<T>, setState];
}

View File

@ -0,0 +1,141 @@
import type { Ref } from 'vue';
import { ref, onBeforeUnmount, onMounted } from 'vue';
import useState from '../../../_util/hooks/useState';
type TouchEventHandler = (e: TouchEvent) => void;
type WheelEventHandler = (e: WheelEvent) => void;
const MIN_SWIPE_DISTANCE = 0.1;
const STOP_SWIPE_DISTANCE = 0.01;
const REFRESH_INTERVAL = 20;
const SPEED_OFF_MULTIPLE = 0.995 ** REFRESH_INTERVAL;
// ================================= Hook =================================
export default function useTouchMove(
domRef: Ref<HTMLDivElement>,
onOffset: (offsetX: number, offsetY: number) => boolean,
) {
const [touchPosition, setTouchPosition] = useState<{ x: number; y: number }>();
const [lastTimestamp, setLastTimestamp] = useState<number>(0);
const [lastTimeDiff, setLastTimeDiff] = useState<number>(0);
const [lastOffset, setLastOffset] = useState<{ x: number; y: number }>();
const motionRef = ref<number>();
// ========================= Events =========================
// >>> Touch events
function onTouchStart(e: TouchEvent) {
const { screenX, screenY } = e.touches[0];
setTouchPosition({ x: screenX, y: screenY });
window.clearInterval(motionRef.value);
}
function onTouchMove(e: TouchEvent) {
if (!touchPosition.value) return;
e.preventDefault();
const { screenX, screenY } = e.touches[0];
setTouchPosition({ x: screenX, y: screenY });
const offsetX = screenX - touchPosition.value.x;
const offsetY = screenY - touchPosition.value.y;
onOffset(offsetX, offsetY);
const now = Date.now();
setLastTimestamp(now);
setLastTimeDiff(now - lastTimestamp.value);
setLastOffset({ x: offsetX, y: offsetY });
}
function onTouchEnd() {
if (!touchPosition.value) return;
setTouchPosition(null);
setLastOffset(null);
// Swipe if needed
if (lastOffset.value) {
const distanceX = lastOffset.value.x / lastTimeDiff.value;
const distanceY = lastOffset.value.y / lastTimeDiff.value;
const absX = Math.abs(distanceX);
const absY = Math.abs(distanceY);
// Skip swipe if low distance
if (Math.max(absX, absY) < MIN_SWIPE_DISTANCE) return;
let currentX = distanceX;
let currentY = distanceY;
motionRef.value = window.setInterval(() => {
if (Math.abs(currentX) < STOP_SWIPE_DISTANCE && Math.abs(currentY) < STOP_SWIPE_DISTANCE) {
window.clearInterval(motionRef.value);
return;
}
currentX *= SPEED_OFF_MULTIPLE;
currentY *= SPEED_OFF_MULTIPLE;
onOffset(currentX * REFRESH_INTERVAL, currentY * REFRESH_INTERVAL);
}, REFRESH_INTERVAL);
}
}
// >>> Wheel event
const lastWheelDirectionRef = ref<'x' | 'y'>();
function onWheel(e: WheelEvent) {
const { deltaX, deltaY } = e;
// Convert both to x & y since wheel only happened on PC
let mixed = 0;
const absX = Math.abs(deltaX);
const absY = Math.abs(deltaY);
if (absX === absY) {
mixed = lastWheelDirectionRef.value === 'x' ? deltaX : deltaY;
} else if (absX > absY) {
mixed = deltaX;
lastWheelDirectionRef.value = 'x';
} else {
mixed = deltaY;
lastWheelDirectionRef.value = 'y';
}
if (onOffset(-mixed, -mixed)) {
e.preventDefault();
}
}
// ========================= Effect =========================
const touchEventsRef = ref<{
onTouchStart: TouchEventHandler;
onTouchMove: TouchEventHandler;
onTouchEnd: TouchEventHandler;
onWheel: WheelEventHandler;
}>({
onTouchStart,
onTouchMove,
onTouchEnd,
onWheel,
});
function onProxyTouchStart(e: TouchEvent) {
touchEventsRef.value.onTouchStart(e);
}
function onProxyTouchMove(e: TouchEvent) {
touchEventsRef.value.onTouchMove(e);
}
function onProxyTouchEnd(e: TouchEvent) {
touchEventsRef.value.onTouchEnd(e);
}
function onProxyWheel(e: WheelEvent) {
touchEventsRef.value.onWheel(e);
}
onMounted(() => {
document.addEventListener('touchmove', onProxyTouchMove, { passive: false });
document.addEventListener('touchend', onProxyTouchEnd, { passive: false });
// No need to clean up since element removed
domRef.value.addEventListener('touchstart', onProxyTouchStart, { passive: false });
domRef.value.addEventListener('wheel', onProxyWheel);
});
onBeforeUnmount(() => {
document.removeEventListener('touchmove', onProxyTouchMove);
document.removeEventListener('touchend', onProxyTouchEnd);
});
}

View File

@ -0,0 +1,11 @@
// base rc-tabs 4.16.6
import Tabs from './Tabs';
import type { TabsProps } from './Tabs';
import TabPane from './TabPanelList/TabPane';
import type { TabPaneProps } from './TabPanelList/TabPane';
export type { TabsProps, TabPaneProps };
export { TabPane };
export default Tabs;

View File

@ -0,0 +1,51 @@
import type { Key, VueNode } from '../../_util/type';
import type { TabPaneProps } from './TabPanelList/TabPane';
export type TabSizeMap = Map<Key, { width: number; height: number; left: number; top: number }>;
export interface TabOffset {
width: number;
height: number;
left: number;
right: number;
top: number;
}
export type TabOffsetMap = Map<Key, TabOffset>;
export type TabPosition = 'left' | 'right' | 'top' | 'bottom';
export interface Tab extends TabPaneProps {
key: string;
node: VueNode;
}
export type RenderTabBar = (props: any, DefaultTabBar: any) => VueNode;
export interface TabsLocale {
dropdownAriaLabel?: string;
removeAriaLabel?: string;
addAriaLabel?: string;
}
export interface EditableConfig {
onEdit: (
type: 'add' | 'remove',
info: { key?: string; event: MouseEvent | KeyboardEvent },
) => void;
showAdd?: boolean;
removeIcon?: () => VueNode;
addIcon?: () => VueNode;
}
export interface AnimatedConfig {
inkBar?: boolean;
tabPane?: boolean;
}
export type OnTabScroll = (info: { direction: 'left' | 'right' | 'top' | 'bottom' }) => void;
export type TabBarExtraPosition = 'left' | 'right';
export type TabBarExtraMap = Partial<Record<TabBarExtraPosition, any>>;
export type TabBarExtraContent = VueNode;

View File

@ -1,186 +0,0 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@tab-prefix-cls: ~'@{ant-prefix}-tabs';
// card style
.@{tab-prefix-cls} {
&&-card &-card-bar &-nav-container {
height: @tabs-card-height;
}
&&-card &-card-bar &-ink-bar {
visibility: hidden;
}
&&-card &-card-bar &-tab {
height: @tabs-card-height;
margin: 0;
margin-right: @tabs-card-gutter;
padding: 0 16px;
line-height: @tabs-card-height - 2px;
background: @tabs-card-head-background;
border: @border-width-base @border-style-base @border-color-split;
border-radius: @border-radius-base @border-radius-base 0 0;
transition: all 0.3s @ease-in-out;
}
&&-card &-card-bar &-tab-active {
height: @tabs-card-height;
color: @tabs-card-active-color;
background: @component-background;
border-color: @border-color-split;
border-bottom: @border-width-base solid @component-background;
&::before {
border-top: @tabs-card-tab-active-border-top;
}
}
&&-card &-card-bar &-tab-disabled {
color: @tabs-card-active-color;
color: @disabled-color;
}
&&-card &-card-bar &-tab-inactive {
padding: 0;
}
&&-card &-card-bar &-nav-wrap {
margin-bottom: 0;
}
&&-card &-card-bar &-tab &-close-x {
width: 16px;
height: 16px;
height: @font-size-base;
margin-right: -5px;
margin-left: 3px;
overflow: hidden;
color: @text-color-secondary;
font-size: @font-size-sm;
vertical-align: middle;
transition: all 0.3s;
&:hover {
color: @heading-color;
}
}
&&-card &-card-content > &-tabpane,
&&-editable-card &-card-content > &-tabpane {
transition: none !important;
&-inactive {
overflow: hidden;
}
}
&&-card &-card-bar &-tab:hover .@{iconfont-css-prefix}-close {
opacity: 1;
}
&-extra-content {
line-height: @tabs-title-font-size * @line-height-base + extract(@tabs-horizontal-padding, 1) *
2;
.@{tab-prefix-cls}-new-tab {
position: relative;
width: 20px;
height: 20px;
color: @text-color;
font-size: 12px;
line-height: 20px;
text-align: center;
border: @border-width-base @border-style-base @border-color-split;
border-radius: @border-radius-sm;
cursor: pointer;
transition: all 0.3s;
&:hover {
color: @tabs-card-active-color;
border-color: @tabs-card-active-color;
}
svg {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
}
}
// https://github.com/ant-design/ant-design/issues/17865
&&-large &-extra-content {
line-height: @tabs-title-font-size-lg * @line-height-base +
extract(@tabs-horizontal-padding-lg, 1) * 2;
}
// https://github.com/ant-design/ant-design/issues/17865
&&-small &-extra-content {
line-height: @tabs-title-font-size-sm * @line-height-base +
extract(@tabs-horizontal-padding-sm, 1) * 2;
}
// https://github.com/ant-design/ant-design/issues/17865
&&-card &-extra-content {
line-height: @tabs-card-height;
}
// https://github.com/ant-design/ant-design/issues/4669
&-vertical&-card &-card-bar&-left-bar,
&-vertical&-card &-card-bar&-right-bar {
.@{tab-prefix-cls}-nav-container {
height: 100%;
}
.@{tab-prefix-cls}-tab {
margin-bottom: 8px;
border-bottom: @border-width-base @border-style-base @border-color-split;
&-active {
padding-bottom: 4px;
}
&:last-child {
margin-bottom: 8px;
}
}
.@{tab-prefix-cls}-new-tab {
width: 90%;
}
}
&-vertical&-card&-left &-card-bar&-left-bar {
.@{tab-prefix-cls}-nav-wrap {
margin-right: 0;
}
.@{tab-prefix-cls}-tab {
margin-right: 1px;
border-right: 0;
border-radius: @border-radius-base 0 0 @border-radius-base;
&-active {
margin-right: -1px;
padding-right: 18px;
}
}
}
&-vertical&-card&-right &-card-bar&-right-bar {
.@{tab-prefix-cls}-nav-wrap {
margin-left: 0;
}
.@{tab-prefix-cls}-tab {
margin-left: 1px;
border-left: 0;
border-radius: 0 @border-radius-base @border-radius-base 0;
&-active {
margin-left: -1px;
padding-left: 18px;
}
}
}
// https://github.com/ant-design/ant-design/issues/9104
& &-card-bar&-bottom-bar &-tab {
height: auto;
border-top: 0;
border-bottom: @border-width-base @border-style-base @border-color-split;
border-radius: 0 0 @border-radius-base @border-radius-base;
}
& &-card-bar&-bottom-bar &-tab-active {
padding-top: 1px;
padding-bottom: 0;
color: @primary-color;
}
}

View File

@ -0,0 +1,97 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@import './index';
.@{tab-prefix-cls}-card {
> .@{tab-prefix-cls}-nav,
> div > .@{tab-prefix-cls}-nav {
.@{tab-prefix-cls}-tab {
margin: 0;
padding: @tabs-card-horizontal-padding;
background: @tabs-card-head-background;
border: @border-width-base @border-style-base @border-color-split;
transition: all @animation-duration-slow @ease-in-out;
&-active {
color: @tabs-card-active-color;
background: @component-background;
}
}
.@{tab-prefix-cls}-ink-bar {
visibility: hidden;
}
}
// ========================== Top & Bottom ==========================
&.@{tab-prefix-cls}-top,
&.@{tab-prefix-cls}-bottom {
> .@{tab-prefix-cls}-nav,
> div > .@{tab-prefix-cls}-nav {
.@{tab-prefix-cls}-tab + .@{tab-prefix-cls}-tab {
margin-left: @tabs-card-gutter;
}
}
}
&.@{tab-prefix-cls}-top {
> .@{tab-prefix-cls}-nav,
> div > .@{tab-prefix-cls}-nav {
.@{tab-prefix-cls}-tab {
border-radius: @border-radius-base @border-radius-base 0 0;
&-active {
border-bottom-color: @component-background;
}
}
}
}
&.@{tab-prefix-cls}-bottom {
> .@{tab-prefix-cls}-nav,
> div > .@{tab-prefix-cls}-nav {
.@{tab-prefix-cls}-tab {
border-radius: 0 0 @border-radius-base @border-radius-base;
&-active {
border-top-color: @component-background;
}
}
}
}
// ========================== Left & Right ==========================
&.@{tab-prefix-cls}-left,
&.@{tab-prefix-cls}-right {
> .@{tab-prefix-cls}-nav,
> div > .@{tab-prefix-cls}-nav {
.@{tab-prefix-cls}-tab + .@{tab-prefix-cls}-tab {
margin-top: @tabs-card-gutter;
}
}
}
&.@{tab-prefix-cls}-left {
> .@{tab-prefix-cls}-nav,
> div > .@{tab-prefix-cls}-nav {
.@{tab-prefix-cls}-tab {
border-radius: @border-radius-base 0 0 @border-radius-base;
&-active {
border-right-color: @component-background;
}
}
}
}
&.@{tab-prefix-cls}-right {
> .@{tab-prefix-cls}-nav,
> div > .@{tab-prefix-cls}-nav {
.@{tab-prefix-cls}-tab {
border-radius: 0 @border-radius-base @border-radius-base 0;
&-active {
border-left-color: @component-background;
}
}
}
}
}

View File

@ -0,0 +1,60 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@import './index';
.@{tab-prefix-cls}-dropdown {
.reset-component();
position: absolute;
top: -9999px;
left: -9999px;
z-index: @zindex-dropdown;
display: block;
&-hidden {
display: none;
}
&-menu {
max-height: 200px;
margin: 0;
padding: @dropdown-edge-child-vertical-padding 0;
overflow-x: hidden;
overflow-y: auto;
text-align: left;
list-style-type: none;
background-color: @dropdown-menu-bg;
background-clip: padding-box;
border-radius: @border-radius-base;
outline: none;
box-shadow: @box-shadow-base;
&-item {
min-width: 120px;
margin: 0;
padding: @dropdown-vertical-padding @control-padding-horizontal;
overflow: hidden;
color: @text-color;
font-weight: normal;
font-size: @dropdown-font-size;
line-height: @dropdown-line-height;
white-space: nowrap;
text-overflow: ellipsis;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: @item-hover-bg;
}
&-disabled {
&,
&:hover {
color: @disabled-color;
background: transparent;
cursor: not-allowed;
}
}
}
}
}

View File

@ -1,446 +1,223 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@import './card-style';
@import './size';
@import './rtl';
@import './position';
@import './dropdown';
@import './card';
@tab-prefix-cls: ~'@{ant-prefix}-tabs';
// Hidden content
.tabs-hidden-content() {
height: 0;
padding: 0 !important;
overflow: hidden;
opacity: 0;
pointer-events: none;
input {
visibility: hidden;
}
}
.@{tab-prefix-cls} {
.reset-component();
display: flex;
overflow: hidden;
// ========================== Navigation ==========================
> .@{tab-prefix-cls}-nav,
> div > .@{tab-prefix-cls}-nav {
position: relative;
overflow: hidden;
.clearfix();
display: flex;
flex: none;
align-items: center;
&-ink-bar {
position: absolute;
bottom: 1px;
left: 0;
z-index: 1;
box-sizing: border-box;
width: 0;
height: 2px;
background-color: @tabs-ink-bar-color;
transform-origin: 0 0;
}
&-bar {
margin: @tabs-bar-margin;
border-bottom: @border-width-base @border-style-base @border-color-split;
outline: none;
transition: padding 0.3s @ease-in-out;
}
&-nav-container {
position: relative;
box-sizing: border-box;
margin-bottom: -1px;
overflow: hidden;
font-size: @tabs-title-font-size;
line-height: @line-height-base;
white-space: nowrap;
transition: padding 0.3s @ease-in-out;
.clearfix();
&-scrolling {
padding-right: @tabs-scrolling-size;
padding-left: @tabs-scrolling-size;
}
}
// https://github.com/ant-design/ant-design/issues/9104
&-bottom &-bottom-bar {
margin-top: 16px;
margin-bottom: 0;
border-top: @border-width-base @border-style-base @border-color-split;
border-bottom: none;
}
&-bottom &-bottom-bar &-ink-bar {
top: 1px;
bottom: auto;
}
&-bottom &-bottom-bar &-nav-container {
margin-top: -1px;
margin-bottom: 0;
}
&-tab-prev,
&-tab-next {
position: absolute;
z-index: 2;
width: 0;
height: 100%;
color: @text-color-secondary;
text-align: center;
background-color: transparent;
border: 0;
cursor: pointer;
opacity: 0;
transition: width 0.3s @ease-in-out, opacity 0.3s @ease-in-out, color 0.3s @ease-in-out;
user-select: none;
pointer-events: none;
&.@{tab-prefix-cls}-tab-arrow-show {
width: @tabs-scrolling-size;
height: 100%;
opacity: 1;
pointer-events: auto;
}
&:hover {
color: @text-color;
}
&-icon {
position: absolute;
top: 50%;
left: 50%;
font-weight: bold;
font-style: normal;
font-variant: normal;
line-height: inherit;
text-align: center;
text-transform: none;
transform: translate(-50%, -50%);
&-target {
display: block;
.iconfont-size-under-12px(10px);
}
}
}
&-tab-btn-disabled {
cursor: not-allowed;
&,
&:hover {
color: @disabled-color;
}
}
&-tab-next {
right: 2px;
}
&-tab-prev {
left: 0;
:root & {
filter: none;
}
}
&-nav-wrap {
margin-bottom: -1px;
overflow: hidden;
}
&-nav-scroll {
overflow: hidden;
white-space: nowrap;
}
&-nav {
.@{tab-prefix-cls}-nav-wrap {
position: relative;
display: inline-block;
box-sizing: border-box;
margin: 0;
padding-left: 0;
list-style: none;
transition: transform 0.3s @ease-in-out;
display: flex;
flex: auto;
align-self: stretch;
overflow: hidden;
white-space: nowrap;
transform: translate(0); // Fix chrome render bug
// >>>>> Ping shadow
&::before,
&::after {
display: table;
content: ' ';
}
&::after {
clear: both;
}
.@{tab-prefix-cls}-tab {
position: relative;
display: inline-block;
box-sizing: border-box;
height: 100%;
margin: @tabs-horizontal-margin;
padding: @tabs-horizontal-padding;
text-decoration: none;
cursor: pointer;
transition: color 0.3s @ease-in-out;
&::before {
position: absolute;
top: -1px;
left: 0;
width: 100%;
border-top: 2px solid transparent;
border-radius: @border-radius-base @border-radius-base 0 0;
transition: all 0.3s;
z-index: 1;
opacity: 0;
transition: opacity @animation-duration-slow;
content: '';
pointer-events: none;
}
}
&:last-child {
margin-right: 0;
.@{tab-prefix-cls}-nav-list {
position: relative;
display: flex;
transition: transform @animation-duration-slow;
}
// >>>>>>>> Operations
.@{tab-prefix-cls}-nav-operations {
display: flex;
align-self: stretch;
&-hidden {
position: absolute;
visibility: hidden;
pointer-events: none;
}
}
.@{tab-prefix-cls}-nav-more {
position: relative;
padding: @tabs-card-horizontal-padding;
background: transparent;
border: 0;
&::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 5px;
transform: translateY(100%);
content: '';
}
}
.@{tab-prefix-cls}-nav-add {
min-width: @tabs-card-height;
padding: 0 @padding-xs;
background: @tabs-card-head-background;
border: @border-width-base @border-style-base @border-color-split;
border-radius: @border-radius-base @border-radius-base 0 0;
outline: none;
cursor: pointer;
transition: all @animation-duration-slow @ease-in-out;
&:hover {
color: @tabs-hover-color;
}
&:active,
&:focus {
color: @tabs-active-color;
}
}
}
&-extra-content {
flex: none;
}
&-centered {
> .@{tab-prefix-cls}-nav,
> div > .@{tab-prefix-cls}-nav {
.@{tab-prefix-cls}-nav-wrap {
&:not([class*='@{tab-prefix-cls}-nav-wrap-ping']) {
justify-content: center;
}
}
}
}
// ============================ InkBar ============================
&-ink-bar {
position: absolute;
background: @tabs-ink-bar-color;
pointer-events: none;
}
// ============================= Tabs =============================
&-tab {
position: relative;
display: inline-flex;
align-items: center;
padding: @tabs-horizontal-padding;
font-size: @tabs-title-font-size;
background: transparent;
border: 0;
outline: none;
cursor: pointer;
&-btn,
&-remove {
&:focus,
&:active {
color: @tabs-active-color;
}
}
&-btn {
outline: none;
transition: all 0.3s;
}
&-remove {
flex: none;
margin-right: -@margin-xss;
margin-left: @margin-xs;
color: @text-color-secondary;
font-size: @font-size-sm;
background: transparent;
border: none;
outline: none;
cursor: pointer;
transition: all @animation-duration-slow;
&:hover {
color: @heading-color;
}
}
&:hover {
color: @tabs-hover-color;
}
&:active {
color: @tabs-active-color;
}
.@{iconfont-css-prefix} {
margin-right: 8px;
}
&-active {
&&-active &-btn {
color: @tabs-highlight-color;
// https://github.com/vueComponent/ant-design-vue/issues/4241
// Remove font-weight to keep pace with antd (#4241)
text-shadow: 0 0 0.25px currentColor;
// font-weight: 500;
}
&-disabled {
&,
&:hover {
&&-disabled {
color: @disabled-color;
cursor: not-allowed;
}
}
&&-disabled &-btn,
&&-disabled &-remove {
&:focus,
&:active {
color: @disabled-color;
}
}
.@{tab-prefix-cls}-large-bar {
.@{tab-prefix-cls}-nav-container {
font-size: @tabs-title-font-size-lg;
& &-remove .@{iconfont-css-prefix} {
margin: 0;
}
.@{tab-prefix-cls}-tab {
padding: @tabs-horizontal-padding-lg;
.@{iconfont-css-prefix} {
margin-right: @margin-sm;
}
}
.@{tab-prefix-cls}-small-bar {
.@{tab-prefix-cls}-nav-container {
font-size: @tabs-title-font-size-sm;
}
.@{tab-prefix-cls}-tab {
padding: @tabs-horizontal-padding-sm;
}
&-tab + &-tab {
margin: @tabs-horizontal-margin;
}
.@{tab-prefix-cls}-centered-bar {
.@{tab-prefix-cls}-nav-wrap {
text-align: center;
}
// =========================== TabPanes ===========================
&-content {
&-holder {
flex: auto;
min-width: 0;
min-height: 0;
}
// Create an empty element to avoid margin collapsing
// https://github.com/ant-design/ant-design/issues/18103
&-content::before {
display: block;
overflow: hidden;
content: '';
}
// Horizontal Content
.@{tab-prefix-cls}-top-content,
.@{tab-prefix-cls}-bottom-content {
width: 100%;
> .@{tab-prefix-cls}-tabpane {
flex-shrink: 0;
width: 100%;
-webkit-backface-visibility: hidden;
opacity: 1;
transition: opacity 0.45s;
}
> .@{tab-prefix-cls}-tabpane-inactive {
.tabs-hidden-content();
}
&.@{tab-prefix-cls}-content-animated {
display: flex;
flex-direction: row;
transition: margin-left 0.3s @ease-in-out;
will-change: margin-left;
}
}
// Vertical Bar
.@{tab-prefix-cls}-left-bar,
.@{tab-prefix-cls}-right-bar {
height: 100%;
border-bottom: 0;
.@{tab-prefix-cls}-tab-arrow-show {
width: 100%;
height: @tabs-scrolling-size;
}
.@{tab-prefix-cls}-tab {
display: block;
float: none;
margin: @tabs-vertical-margin;
padding: @tabs-vertical-padding;
&:last-child {
margin-bottom: 0;
&-animated {
transition: margin @animation-duration-slow;
}
}
.@{tab-prefix-cls}-extra-content {
text-align: center;
}
.@{tab-prefix-cls}-nav-scroll {
width: auto;
}
.@{tab-prefix-cls}-nav-container,
.@{tab-prefix-cls}-nav-wrap {
height: 100%;
}
.@{tab-prefix-cls}-nav-container {
margin-bottom: 0;
&.@{tab-prefix-cls}-nav-container-scrolling {
padding: @tabs-scrolling-size 0;
}
}
.@{tab-prefix-cls}-nav-wrap {
margin-bottom: 0;
}
.@{tab-prefix-cls}-nav {
&-tabpane {
flex: none;
width: 100%;
}
.@{tab-prefix-cls}-ink-bar {
top: 0;
bottom: auto;
left: auto;
width: 2px;
height: 0;
}
.@{tab-prefix-cls}-tab-next {
right: 0;
bottom: 0;
width: 100%;
height: @tabs-scrolling-size;
}
.@{tab-prefix-cls}-tab-prev {
top: 0;
width: 100%;
height: @tabs-scrolling-size;
outline: none;
}
}
// Vertical Content
.@{tab-prefix-cls}-left-content,
.@{tab-prefix-cls}-right-content {
width: auto;
margin-top: 0 !important;
overflow: hidden;
}
// Vertical - Left
.@{tab-prefix-cls}-left-bar {
float: left;
margin-right: -1px;
margin-bottom: 0;
border-right: @border-width-base @border-style-base @border-color-split;
.@{tab-prefix-cls}-tab {
text-align: right;
}
.@{tab-prefix-cls}-nav-container {
margin-right: -1px;
}
.@{tab-prefix-cls}-nav-wrap {
margin-right: -1px;
}
.@{tab-prefix-cls}-ink-bar {
right: 1px;
}
}
.@{tab-prefix-cls}-left-content {
padding-left: 24px;
border-left: @border-width-base @border-style-base @border-color-split;
}
// Vertical - Right
.@{tab-prefix-cls}-right-bar {
float: right;
margin-bottom: 0;
margin-left: -1px;
border-left: @border-width-base @border-style-base @border-color-split;
.@{tab-prefix-cls}-nav-container {
margin-left: -1px;
}
.@{tab-prefix-cls}-nav-wrap {
margin-left: -1px;
}
.@{tab-prefix-cls}-ink-bar {
left: 1px;
}
}
.@{tab-prefix-cls}-right-content {
padding-right: 24px;
border-right: @border-width-base @border-style-base @border-color-split;
}
}
.@{tab-prefix-cls}-top .@{tab-prefix-cls}-ink-bar-animated,
.@{tab-prefix-cls}-bottom .@{tab-prefix-cls}-ink-bar-animated {
transition: transform 0.3s @ease-in-out, width 0.2s @ease-in-out, left 0.3s @ease-in-out;
}
.@{tab-prefix-cls}-left .@{tab-prefix-cls}-ink-bar-animated,
.@{tab-prefix-cls}-right .@{tab-prefix-cls}-ink-bar-animated {
transition: transform 0.3s @ease-in-out, height 0.2s @ease-in-out, top 0.3s @ease-in-out;
}
// No animation
.tabs-no-animation() {
> .@{tab-prefix-cls}-content-animated {
margin-left: 0 !important;
transform: none !important;
}
> .@{tab-prefix-cls}-tabpane-inactive {
.tabs-hidden-content();
}
}
.no-flex,
.@{tab-prefix-cls}-no-animation {
> .@{tab-prefix-cls}-content {
.tabs-no-animation();
}
}
.@{tab-prefix-cls}-left-content,
.@{tab-prefix-cls}-right-content {
.tabs-no-animation();
}

View File

@ -0,0 +1,195 @@
@import './index';
.@{tab-prefix-cls} {
// ========================== Top & Bottom ==========================
&-top,
&-bottom {
flex-direction: column;
> .@{tab-prefix-cls}-nav,
> div > .@{tab-prefix-cls}-nav {
margin: @tabs-bar-margin;
&::before {
position: absolute;
right: 0;
left: 0;
border-bottom: @border-width-base @border-style-base @border-color-split;
content: '';
}
.@{tab-prefix-cls}-ink-bar {
height: 2px;
&-animated {
transition: width @animation-duration-slow, left @animation-duration-slow,
right @animation-duration-slow;
}
}
.@{tab-prefix-cls}-nav-wrap {
&::before,
&::after {
top: 0;
bottom: 0;
width: 30px;
}
&::before {
left: 0;
box-shadow: inset 10px 0 8px -8px fade(@shadow-color, 8%);
}
&::after {
right: 0;
box-shadow: inset -10px 0 8px -8px fade(@shadow-color, 8%);
}
&.@{tab-prefix-cls}-nav-wrap-ping-left::before {
opacity: 1;
}
&.@{tab-prefix-cls}-nav-wrap-ping-right::after {
opacity: 1;
}
}
}
}
&-top {
> .@{tab-prefix-cls}-nav,
> div > .@{tab-prefix-cls}-nav {
&::before {
bottom: 0;
}
.@{tab-prefix-cls}-ink-bar {
bottom: 0;
}
}
}
&-bottom {
> .@{tab-prefix-cls}-nav,
> div > .@{tab-prefix-cls}-nav {
order: 1;
margin-top: @margin-md;
margin-bottom: 0;
&::before {
top: 0;
}
.@{tab-prefix-cls}-ink-bar {
top: 0;
}
}
> .@{tab-prefix-cls}-content-holder,
> div > .@{tab-prefix-cls}-content-holder {
order: 0;
}
}
// ========================== Left & Right ==========================
&-left,
&-right {
> .@{tab-prefix-cls}-nav,
> div > .@{tab-prefix-cls}-nav {
flex-direction: column;
min-width: 50px;
// >>>>>>>>>>> Tab
.@{tab-prefix-cls}-tab {
padding: @tabs-vertical-padding;
text-align: center;
}
.@{tab-prefix-cls}-tab + .@{tab-prefix-cls}-tab {
margin: @tabs-vertical-margin;
}
// >>>>>>>>>>> Nav
.@{tab-prefix-cls}-nav-wrap {
flex-direction: column;
&::before,
&::after {
right: 0;
left: 0;
height: 30px;
}
&::before {
top: 0;
box-shadow: inset 0 10px 8px -8px fade(@shadow-color, 8%);
}
&::after {
bottom: 0;
box-shadow: inset 0 -10px 8px -8px fade(@shadow-color, 8%);
}
&.@{tab-prefix-cls}-nav-wrap-ping-top::before {
opacity: 1;
}
&.@{tab-prefix-cls}-nav-wrap-ping-bottom::after {
opacity: 1;
}
}
// >>>>>>>>>>> Ink Bar
.@{tab-prefix-cls}-ink-bar {
width: 2px;
&-animated {
transition: height @animation-duration-slow, top @animation-duration-slow;
}
}
.@{tab-prefix-cls}-nav-list,
.@{tab-prefix-cls}-nav-operations {
flex: 1 0 auto; // fix safari scroll problem
flex-direction: column;
}
}
}
&-left {
> .@{tab-prefix-cls}-nav,
> div > .@{tab-prefix-cls}-nav {
.@{tab-prefix-cls}-ink-bar {
right: 0;
}
}
> .@{tab-prefix-cls}-content-holder,
> div > .@{tab-prefix-cls}-content-holder {
margin-left: -@border-width-base;
border-left: @border-width-base @border-style-base @border-color-split;
> .@{tab-prefix-cls}-content > .@{tab-prefix-cls}-tabpane {
padding-left: @padding-lg;
}
}
}
&-right {
> .@{tab-prefix-cls}-nav,
> div > .@{tab-prefix-cls}-nav {
order: 1;
.@{tab-prefix-cls}-ink-bar {
left: 0;
}
}
> .@{tab-prefix-cls}-content-holder,
> div > .@{tab-prefix-cls}-content-holder {
order: 0;
margin-right: -@border-width-base;
border-right: @border-width-base @border-style-base @border-color-split;
> .@{tab-prefix-cls}-content > .@{tab-prefix-cls}-tabpane {
padding-right: @padding-lg;
}
}
}
}

View File

@ -0,0 +1,79 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@tab-prefix-cls: ~'@{ant-prefix}-tabs';
.@{tab-prefix-cls} {
&-rtl {
direction: rtl;
.@{tab-prefix-cls}-nav {
.@{tab-prefix-cls}-tab {
margin: @tabs-horizontal-margin-rtl;
&:last-of-type {
margin-left: 0;
}
.@{iconfont-css-prefix} {
margin-right: 0;
margin-left: @margin-sm;
}
.@{tab-prefix-cls}-tab-remove {
margin-right: @margin-xs;
margin-left: -@margin-xss;
.@{iconfont-css-prefix} {
margin: 0;
}
}
}
}
&.@{tab-prefix-cls}-left {
> .@{tab-prefix-cls}-nav {
order: 1;
}
> .@{tab-prefix-cls}-content-holder {
order: 0;
}
}
&.@{tab-prefix-cls}-right {
> .@{tab-prefix-cls}-nav {
order: 0;
}
> .@{tab-prefix-cls}-content-holder {
order: 1;
}
}
}
// ====================== Card ======================
&-card {
&.@{tab-prefix-cls}-top,
&.@{tab-prefix-cls}-bottom {
> .@{tab-prefix-cls}-nav,
> div > .@{tab-prefix-cls}-nav {
.@{tab-prefix-cls}-tab + .@{tab-prefix-cls}-tab {
.@{tab-prefix-cls}-rtl& {
margin-right: 0;
margin-left: @tabs-card-gutter;
}
}
}
}
}
}
.@{tab-prefix-cls}-dropdown {
&-rtl {
direction: rtl;
}
&-menu-item {
.@{tab-prefix-cls}-dropdown-rtl & {
text-align: right;
}
}
}

View File

@ -0,0 +1,41 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@import './index';
.@{tab-prefix-cls} {
&-small {
> .@{tab-prefix-cls}-nav {
.@{tab-prefix-cls}-tab {
padding: @tabs-horizontal-padding-sm;
font-size: @tabs-title-font-size-sm;
}
}
}
&-large {
> .@{tab-prefix-cls}-nav {
.@{tab-prefix-cls}-tab {
padding: @tabs-horizontal-padding-lg;
font-size: @tabs-title-font-size-lg;
}
}
}
&-card {
&.@{tab-prefix-cls}-small {
> .@{tab-prefix-cls}-nav {
.@{tab-prefix-cls}-tab {
padding: @tabs-card-horizontal-padding-sm;
}
}
}
&.@{tab-prefix-cls}-large {
> .@{tab-prefix-cls}-nav {
.@{tab-prefix-cls}-tab {
padding: @tabs-card-horizontal-padding-lg;
}
}
}
}
}

View File

@ -1,181 +0,0 @@
import type { PropType } from 'vue';
import { defineComponent, inject } from 'vue';
import { tuple } from '../_util/type';
import CloseOutlined from '@ant-design/icons-vue/CloseOutlined';
import PlusOutlined from '@ant-design/icons-vue/PlusOutlined';
import VcTabs, { TabPane } from '../vc-tabs/src';
import TabContent from '../vc-tabs/src/TabContent';
import PropTypes, { withUndefined } from '../_util/vue-types';
import {
getComponent,
getOptionProps,
filterEmpty,
getPropsData,
getSlot,
} from '../_util/props-util';
import { cloneElement } from '../_util/vnode';
import isValid from '../_util/isValid';
import { defaultConfigProvider } from '../config-provider';
import TabBar from './TabBar';
export default defineComponent({
TabPane,
name: 'ATabs',
inheritAttrs: false,
props: {
prefixCls: PropTypes.string,
activeKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
defaultActiveKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
hideAdd: PropTypes.looseBool.def(false),
centered: PropTypes.looseBool.def(false),
tabBarStyle: PropTypes.object,
tabBarExtraContent: PropTypes.any,
destroyInactiveTabPane: PropTypes.looseBool.def(false),
type: PropTypes.oneOf(tuple('line', 'card', 'editable-card')),
tabPosition: PropTypes.oneOf(['top', 'right', 'bottom', 'left']).def('top'),
size: PropTypes.oneOf(['default', 'small', 'large']),
animated: withUndefined(PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object])),
tabBarGutter: PropTypes.number,
renderTabBar: PropTypes.func,
onChange: {
type: Function as PropType<(activeKey: string) => void>,
},
onTabClick: PropTypes.func,
onPrevClick: {
type: Function as PropType<(e: MouseEvent) => void>,
},
onNextClick: {
type: Function as PropType<(e: MouseEvent) => void>,
},
onEdit: {
type: Function as PropType<
(targetKey: string | MouseEvent, action: 'add' | 'remove') => void
>,
},
},
emits: ['update:activeKey', 'edit', 'change'],
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
};
},
methods: {
removeTab(targetKey: string, e: MouseEvent) {
e.stopPropagation();
if (isValid(targetKey)) {
this.$emit('edit', targetKey, 'remove');
}
},
handleChange(activeKey: string) {
this.$emit('update:activeKey', activeKey);
this.$emit('change', activeKey);
},
createNewTab(targetKey: MouseEvent) {
this.$emit('edit', targetKey, 'add');
},
},
render() {
const props = getOptionProps(this);
const {
prefixCls: customizePrefixCls,
size,
type = 'line',
tabPosition,
animated = true,
hideAdd,
renderTabBar,
} = props;
const { class: className, ...restProps } = this.$attrs;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('tabs', customizePrefixCls);
const children = filterEmpty(getSlot(this));
let tabBarExtraContent = getComponent(this, 'tabBarExtraContent');
let tabPaneAnimated = typeof animated === 'object' ? animated.tabPane : animated;
// card tabs should not have animation
if (type !== 'line') {
tabPaneAnimated = 'animated' in props ? tabPaneAnimated : false;
}
const cls = {
[className as string]: className,
[`${prefixCls}-vertical`]: tabPosition === 'left' || tabPosition === 'right',
[`${prefixCls}-${size}`]: !!size,
[`${prefixCls}-card`]: type.indexOf('card') >= 0,
[`${prefixCls}-${type}`]: true,
[`${prefixCls}-no-animation`]: !tabPaneAnimated,
};
// only card type tabs can be added and closed
let childrenWithClose = [];
if (type === 'editable-card') {
childrenWithClose = [];
children.forEach((child, index) => {
const props = getPropsData(child) as any;
let closable = props.closable;
closable = typeof closable === 'undefined' ? true : closable;
const closeIcon = closable ? (
<CloseOutlined
class={`${prefixCls}-close-x`}
onClick={e => this.removeTab(child.key, e)}
/>
) : null;
childrenWithClose.push(
cloneElement(child, {
tab: (
<div class={closable ? undefined : `${prefixCls}-tab-unclosable`}>
{getComponent(child, 'tab')}
{closeIcon}
</div>
),
key: child.key || index,
}),
);
});
// Add new tab handler
if (!hideAdd) {
tabBarExtraContent = (
<span>
<PlusOutlined class={`${prefixCls}-new-tab`} onClick={this.createNewTab} />
{tabBarExtraContent}
</span>
);
}
}
tabBarExtraContent = tabBarExtraContent ? (
<div class={`${prefixCls}-extra-content`}>{tabBarExtraContent}</div>
) : null;
const renderTabBarSlot = renderTabBar || this.$slots.renderTabBar;
const tabBarProps = {
...props,
prefixCls,
tabBarExtraContent,
renderTabBar: renderTabBarSlot,
...restProps,
children,
};
const contentCls = {
[`${prefixCls}-${tabPosition}-content`]: true,
[`${prefixCls}-card-content`]: type.indexOf('card') >= 0,
};
const tabsProps = {
...props,
prefixCls,
tabBarPosition: tabPosition,
// https://github.com/vueComponent/ant-design-vue/issues/2030
// tabBarProps renderTabBar on
// keybabel jsx mergeTabBar tabBarProps
renderTabBar: () => <TabBar key="tabBar" {...tabBarProps} />,
renderTabContent: () => (
<TabContent class={contentCls} animated={tabPaneAnimated} animatedWithMargin />
),
children: childrenWithClose.length > 0 ? childrenWithClose : children,
...restProps,
onChange: this.handleChange,
class: cls,
};
return <VcTabs {...tabsProps} />;
},
});

View File

@ -43,6 +43,7 @@ const Overflow = defineComponent({
name: 'Overflow',
inheritAttrs: false,
props: {
id: String,
prefixCls: String,
data: Array,
itemKey: [String, Number, Function] as PropType<Key | ((item: any) => Key)>,
@ -245,6 +246,7 @@ const Overflow = defineComponent({
prefixCls = 'rc-overflow',
suffix,
component: Component = 'div' as any,
id,
} = props;
const { class: className, style, ...restAttrs } = attrs;
let suffixStyle: CSSProperties = {};
@ -341,6 +343,7 @@ const Overflow = defineComponent({
const overflowNode = () => (
<Component
id={id}
class={classNames(!invalidate.value && prefixCls, className)}
style={style}
{...restAttrs}

View File

@ -10,6 +10,7 @@ export default defineComponent({
props: {
component: PropTypes.any,
title: PropTypes.any,
id: String,
},
setup(props, { slots, attrs }) {
const context = useInjectOverflowContext();

View File

@ -1,22 +0,0 @@
import InkTabBarNode from './InkTabBarNode';
import TabBarTabsNode from './TabBarTabsNode';
import TabBarRootNode from './TabBarRootNode';
import SaveRef from './SaveRef';
function noop() {}
const InkTabBar = (_, { attrs }) => {
const { onTabClick = noop, ...props } = attrs;
return (
<SaveRef
children={(saveRef, getRef) => (
<TabBarRootNode saveRef={saveRef} {...props}>
<TabBarTabsNode onTabClick={onTabClick} saveRef={saveRef} {...props} />
<InkTabBarNode saveRef={saveRef} getRef={getRef} {...props} />
</TabBarRootNode>
)}
/>
);
};
InkTabBar.inheritAttrs = false;
export default InkTabBar;

View File

@ -1,121 +0,0 @@
import PropTypes from '../../_util/vue-types';
import {
setTransform,
isTransform3dSupported,
getLeft,
getStyle,
getTop,
getActiveIndex,
} from './utils';
import BaseMixin from '../../_util/BaseMixin';
function componentDidUpdate(component, init) {
const { styles = {}, panels, activeKey, direction } = component.$props;
const rootNode = component.getRef('root');
const wrapNode = component.getRef('nav') || rootNode;
const inkBarNode = component.getRef('inkBar');
const activeTab = component.getRef('activeTab');
const inkBarNodeStyle = inkBarNode.style;
const tabBarPosition = component.$props.tabBarPosition;
const activeIndex = getActiveIndex(panels, activeKey);
if (init) {
// prevent mount animation
inkBarNodeStyle.display = 'none';
}
if (activeTab) {
const tabNode = activeTab;
const transformSupported = isTransform3dSupported(inkBarNodeStyle);
// Reset current style
setTransform(inkBarNodeStyle, '');
inkBarNodeStyle.width = '';
inkBarNodeStyle.height = '';
inkBarNodeStyle.left = '';
inkBarNodeStyle.top = '';
inkBarNodeStyle.bottom = '';
inkBarNodeStyle.right = '';
if (tabBarPosition === 'top' || tabBarPosition === 'bottom') {
let left = getLeft(tabNode, wrapNode);
let width = tabNode.offsetWidth;
// If tabNode'width width equal to wrapNode'width when tabBarPosition is top or bottom
// It means no css working, then ink bar should not have width until css is loaded
// Fix https://github.com/ant-design/ant-design/issues/7564
if (width === rootNode.offsetWidth) {
width = 0;
} else if (styles.inkBar && styles.inkBar.width !== undefined) {
width = parseFloat(styles.inkBar.width, 10);
if (width) {
left += (tabNode.offsetWidth - width) / 2;
}
}
if (direction === 'rtl') {
left = getStyle(tabNode, 'margin-left') - left;
}
// use 3d gpu to optimize render
if (transformSupported) {
setTransform(inkBarNodeStyle, `translate3d(${left}px,0,0)`);
} else {
inkBarNodeStyle.left = `${left}px`;
}
inkBarNodeStyle.width = `${width}px`;
} else {
let top = getTop(tabNode, wrapNode, true);
let height = tabNode.offsetHeight;
if (styles.inkBar && styles.inkBar.height !== undefined) {
height = parseFloat(styles.inkBar.height, 10);
if (height) {
top += (tabNode.offsetHeight - height) / 2;
}
}
if (transformSupported) {
setTransform(inkBarNodeStyle, `translate3d(0,${top}px,0)`);
inkBarNodeStyle.top = '0';
} else {
inkBarNodeStyle.top = `${top}px`;
}
inkBarNodeStyle.height = `${height}px`;
}
}
inkBarNodeStyle.display = activeIndex !== -1 ? 'block' : 'none';
}
export default {
name: 'InkTabBarNode',
mixins: [BaseMixin],
inheritAttrs: false,
props: {
inkBarAnimated: {
type: Boolean,
default: true,
},
direction: PropTypes.string,
prefixCls: String,
styles: Object,
tabBarPosition: String,
saveRef: PropTypes.func.def(() => {}),
getRef: PropTypes.func.def(() => {}),
panels: PropTypes.array,
activeKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
},
updated() {
this.$nextTick(() => {
componentDidUpdate(this);
});
},
mounted() {
this.$nextTick(() => {
componentDidUpdate(this, true);
});
},
render() {
const { prefixCls, styles = {}, inkBarAnimated } = this;
const className = `${prefixCls}-ink-bar`;
const classes = {
[className]: true,
[inkBarAnimated ? `${className}-animated` : `${className}-no-animated`]: true,
};
return <div style={styles.inkBar} class={classes} key="inkBar" ref={this.saveRef('inkBar')} />;
},
};

View File

@ -1,18 +0,0 @@
export default {
/**
* LEFT
*/
LEFT: 37, // also NUM_WEST
/**
* UP
*/
UP: 38, // also NUM_NORTH
/**
* RIGHT
*/
RIGHT: 39, // also NUM_EAST
/**
* DOWN
*/
DOWN: 40, // also NUM_SOUTH
};

View File

@ -1,27 +0,0 @@
import PropTypes from '../../_util/vue-types';
export default {
props: {
children: PropTypes.func.def(() => null),
},
methods: {
getRef(name) {
return this[name];
},
saveRef(name) {
return node => {
if (node) {
this[name] = node;
}
};
},
},
render() {
// newfunction
const saveRef = name => this.saveRef(name);
const getRef = name => this.getRef(name);
return this.children(saveRef, getRef);
},
};

View File

@ -1,26 +0,0 @@
import { defineComponent } from 'vue';
import InkTabBarNode from './InkTabBarNode';
import TabBarTabsNode from './TabBarTabsNode';
import TabBarRootNode from './TabBarRootNode';
import ScrollableTabBarNode from './ScrollableTabBarNode';
import SaveRef from './SaveRef';
export default defineComponent({
name: 'ScrollableInkTabBar',
inheritAttrs: false,
render() {
const { children: renderTabBarNode } = this.$attrs;
return (
<SaveRef
children={(saveRef, getRef) => (
<TabBarRootNode saveRef={saveRef} {...this.$attrs}>
<ScrollableTabBarNode saveRef={saveRef} getRef={getRef} {...this.$attrs}>
<TabBarTabsNode saveRef={saveRef} {...{ ...this.$attrs, renderTabBarNode }} />
<InkTabBarNode saveRef={saveRef} getRef={getRef} {...this.$attrs} />
</ScrollableTabBarNode>
</TabBarRootNode>
)}
/>
);
},
});

View File

@ -1,20 +0,0 @@
import ScrollableTabBarNode from './ScrollableTabBarNode';
import TabBarRootNode from './TabBarRootNode';
import TabBarTabsNode from './TabBarTabsNode';
import SaveRef from './SaveRef';
const ScrollableTabBar = (_, { attrs }) => {
return (
<SaveRef
children={(saveRef, getRef) => (
<TabBarRootNode saveRef={saveRef} {...attrs}>
<ScrollableTabBarNode saveRef={saveRef} getRef={getRef} {...attrs}>
<TabBarTabsNode saveRef={saveRef} {...attrs} />
</ScrollableTabBarNode>
</TabBarRootNode>
)}
/>
);
};
ScrollableTabBar.inheritAttrs = false;
export default ScrollableTabBar;

View File

@ -1,335 +0,0 @@
import debounce from 'lodash-es/debounce';
import ResizeObserver from 'resize-observer-polyfill';
import PropTypes from '../../_util/vue-types';
import BaseMixin from '../../_util/BaseMixin';
import { getComponent, getSlot } from '../../_util/props-util';
import { setTransform, isTransform3dSupported } from './utils';
export default {
name: 'ScrollableTabBarNode',
mixins: [BaseMixin],
inheritAttrs: false,
props: {
activeKey: PropTypes.any,
getRef: PropTypes.func.def(() => {}),
saveRef: PropTypes.func.def(() => {}),
tabBarPosition: PropTypes.oneOf(['left', 'right', 'top', 'bottom']).def('left'),
prefixCls: PropTypes.string.def(''),
scrollAnimated: PropTypes.looseBool.def(true),
navWrapper: PropTypes.func.def(arg => arg),
prevIcon: PropTypes.any,
nextIcon: PropTypes.any,
direction: PropTypes.string,
},
data() {
this.offset = 0;
this.prevProps = { ...this.$props };
return {
next: false,
prev: false,
};
},
watch: {
tabBarPosition() {
this.tabBarPositionChange = true;
this.$nextTick(() => {
this.setOffset(0);
});
},
},
mounted() {
this.$nextTick(() => {
this.updatedCal();
this.debouncedResize = debounce(() => {
this.setNextPrev();
this.scrollToActiveTab();
}, 200);
this.resizeObserver = new ResizeObserver(this.debouncedResize);
this.resizeObserver.observe(this.$props.getRef('container'));
});
},
updated() {
this.$nextTick(() => {
this.updatedCal(this.prevProps);
this.prevProps = { ...this.$props };
});
},
beforeUnmount() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
if (this.debouncedResize && this.debouncedResize.cancel) {
this.debouncedResize.cancel();
}
},
methods: {
updatedCal(prevProps) {
const props = this.$props;
if (prevProps && prevProps.tabBarPosition !== props.tabBarPosition) {
this.setOffset(0);
return;
}
// wait next, prev show hide
if (this.isNextPrevShown(this.$data) !== this.isNextPrevShown(this.setNextPrev())) {
this.$forceUpdate();
this.$nextTick(() => {
this.scrollToActiveTab();
});
} else if (!prevProps || props.activeKey !== prevProps.activeKey) {
// can not use props.activeKey
this.scrollToActiveTab();
}
},
setNextPrev() {
const navNode = this.$props.getRef('nav');
const navTabsContainer = this.$props.getRef('navTabsContainer');
const navNodeWH = this.getScrollWH(navTabsContainer || navNode);
// Add 1px to fix `offsetWidth` with decimal in Chrome not correct handle
// https://github.com/ant-design/ant-design/issues/13423
const containerWH = this.getOffsetWH(this.$props.getRef('container')) + 1;
const navWrapNodeWH = this.getOffsetWH(this.$props.getRef('navWrap'));
let { offset } = this;
const minOffset = containerWH - navNodeWH;
let { next, prev } = this;
if (minOffset >= 0) {
next = false;
this.setOffset(0, false);
offset = 0;
} else if (minOffset < offset) {
next = true;
} else {
next = false;
// Fix https://github.com/ant-design/ant-design/issues/8861
// Test with container offset which is stable
// and set the offset of the nav wrap node
const realOffset = navWrapNodeWH - navNodeWH;
this.setOffset(realOffset, false);
offset = realOffset;
}
if (offset < 0) {
prev = true;
} else {
prev = false;
}
this.setNext(next);
this.setPrev(prev);
return {
next,
prev,
};
},
getOffsetWH(node) {
const tabBarPosition = this.$props.tabBarPosition;
let prop = 'offsetWidth';
if (tabBarPosition === 'left' || tabBarPosition === 'right') {
prop = 'offsetHeight';
}
return node[prop];
},
getScrollWH(node) {
const tabBarPosition = this.tabBarPosition;
let prop = 'scrollWidth';
if (tabBarPosition === 'left' || tabBarPosition === 'right') {
prop = 'scrollHeight';
}
return node[prop];
},
getOffsetLT(node) {
const tabBarPosition = this.$props.tabBarPosition;
let prop = 'left';
if (tabBarPosition === 'left' || tabBarPosition === 'right') {
prop = 'top';
}
return node.getBoundingClientRect()[prop];
},
setOffset(offset, checkNextPrev = true) {
let target = Math.min(0, offset);
if (this.offset !== target) {
this.offset = target;
let navOffset = {};
const tabBarPosition = this.$props.tabBarPosition;
const navStyle = this.$props.getRef('nav').style;
const transformSupported = isTransform3dSupported(navStyle);
if (tabBarPosition === 'left' || tabBarPosition === 'right') {
if (transformSupported) {
navOffset = {
value: `translate3d(0,${target}px,0)`,
};
} else {
navOffset = {
name: 'top',
value: `${target}px`,
};
}
} else if (transformSupported) {
if (this.$props.direction === 'rtl') {
target = -target;
}
navOffset = {
value: `translate3d(${target}px,0,0)`,
};
} else {
navOffset = {
name: 'left',
value: `${target}px`,
};
}
if (transformSupported) {
setTransform(navStyle, navOffset.value);
} else {
navStyle[navOffset.name] = navOffset.value;
}
if (checkNextPrev) {
this.setNextPrev();
}
}
},
setPrev(v) {
if (this.prev !== v) {
this.prev = v;
}
},
setNext(v) {
if (this.next !== v) {
this.next = v;
}
},
isNextPrevShown(state) {
if (state) {
return state.next || state.prev;
}
return this.next || this.prev;
},
prevTransitionEnd(e) {
if (e.propertyName !== 'opacity') {
return;
}
const container = this.$props.getRef('container');
this.scrollToActiveTab({
target: container,
currentTarget: container,
});
},
scrollToActiveTab(e) {
const activeTab = this.$props.getRef('activeTab');
const navWrap = this.$props.getRef('navWrap');
if ((e && e.target !== e.currentTarget) || !activeTab) {
return;
}
// when not scrollable or enter scrollable first time, don't emit scrolling
const needToSroll = this.isNextPrevShown() && this.lastNextPrevShown;
this.lastNextPrevShown = this.isNextPrevShown();
if (!needToSroll) {
return;
}
const activeTabWH = this.getScrollWH(activeTab);
const navWrapNodeWH = this.getOffsetWH(navWrap);
let { offset } = this;
const wrapOffset = this.getOffsetLT(navWrap);
const activeTabOffset = this.getOffsetLT(activeTab);
if (wrapOffset > activeTabOffset) {
offset += wrapOffset - activeTabOffset;
this.setOffset(offset);
} else if (wrapOffset + navWrapNodeWH < activeTabOffset + activeTabWH) {
offset -= activeTabOffset + activeTabWH - (wrapOffset + navWrapNodeWH);
this.setOffset(offset);
}
},
prevClick(e) {
this.__emit('prevClick', e);
const navWrapNode = this.$props.getRef('navWrap');
const navWrapNodeWH = this.getOffsetWH(navWrapNode);
const { offset } = this;
this.setOffset(offset + navWrapNodeWH);
},
nextClick(e) {
this.__emit('nextClick', e);
const navWrapNode = this.$props.getRef('navWrap');
const navWrapNodeWH = this.getOffsetWH(navWrapNode);
const { offset } = this;
this.setOffset(offset - navWrapNodeWH);
},
},
render() {
const { next, prev } = this;
const { prefixCls, scrollAnimated, navWrapper } = this.$props;
const prevIcon = getComponent(this, 'prevIcon');
const nextIcon = getComponent(this, 'nextIcon');
const showNextPrev = prev || next;
const prevButton = (
<span
onClick={prev && this.prevClick}
unselectable="unselectable"
class={{
[`${prefixCls}-tab-prev`]: 1,
[`${prefixCls}-tab-btn-disabled`]: !prev,
[`${prefixCls}-tab-arrow-show`]: showNextPrev,
}}
onTransitionend={this.prevTransitionEnd}
>
{prevIcon || <span class={`${prefixCls}-tab-prev-icon`} />}
</span>
);
const nextButton = (
<span
onClick={next && this.nextClick}
unselectable="unselectable"
class={{
[`${prefixCls}-tab-next`]: 1,
[`${prefixCls}-tab-btn-disabled`]: !next,
[`${prefixCls}-tab-arrow-show`]: showNextPrev,
}}
>
{nextIcon || <span class={`${prefixCls}-tab-next-icon`} />}
</span>
);
const navClassName = `${prefixCls}-nav`;
const navClasses = {
[navClassName]: true,
[scrollAnimated ? `${navClassName}-animated` : `${navClassName}-no-animated`]: true,
};
return (
<div
class={{
[`${prefixCls}-nav-container`]: 1,
[`${prefixCls}-nav-container-scrolling`]: showNextPrev,
}}
key="container"
ref={this.saveRef('container')}
>
{prevButton}
{nextButton}
<div class={`${prefixCls}-nav-wrap`} ref={this.saveRef('navWrap')}>
<div class={`${prefixCls}-nav-scroll`}>
<div class={navClasses} ref={this.saveRef('nav')}>
{navWrapper(getSlot(this))}
</div>
</div>
</div>
</div>
);
},
};

View File

@ -1,44 +0,0 @@
import PropTypes from '../../_util/vue-types';
import KeyCode from '../../_util/KeyCode';
import { getSlot } from '../../_util/props-util';
const sentinelStyle = { width: 0, height: 0, overflow: 'hidden', position: 'absolute' };
export default {
name: 'Sentinel',
props: {
setRef: PropTypes.func,
prevElement: PropTypes.any,
nextElement: PropTypes.any,
},
methods: {
onKeyDown({ target, which, shiftKey }) {
const { nextElement, prevElement } = this.$props;
if (which !== KeyCode.TAB || document.activeElement !== target) return;
// Tab next
if (!shiftKey && nextElement) {
nextElement.focus();
}
// Tab prev
if (shiftKey && prevElement) {
prevElement.focus();
}
},
},
render() {
const { setRef } = this.$props;
return (
<div
tabindex={0}
ref={setRef}
style={sentinelStyle}
onKeydown={this.onKeyDown}
role="presentation"
>
{getSlot(this)}
</div>
);
},
};

View File

@ -1,19 +0,0 @@
import TabBarRootNode from './TabBarRootNode';
import TabBarTabsNode from './TabBarTabsNode';
import SaveRef from './SaveRef';
export default {
name: 'TabBar',
inheritAttrs: false,
render() {
return (
<SaveRef
children={saveRef => (
<TabBarRootNode saveRef={saveRef} {...this.$attrs}>
<TabBarTabsNode saveRef={saveRef} {...this.$attrs} />
</TabBarRootNode>
)}
/>
);
},
};

View File

@ -1,61 +0,0 @@
import { cloneElement } from '../../_util/vnode';
import PropTypes from '../../_util/vue-types';
import BaseMixin from '../../_util/BaseMixin';
import { getSlot } from '../../_util/props-util';
import { getDataAttr } from './utils';
function noop() {}
export default {
name: 'TabBarRootNode',
mixins: [BaseMixin],
inheritAttrs: false,
props: {
saveRef: PropTypes.func.def(noop),
getRef: PropTypes.func.def(noop),
prefixCls: PropTypes.string.def(''),
tabBarPosition: PropTypes.string.def('top'),
extraContent: PropTypes.any,
},
methods: {
onKeyDown(e) {
this.__emit('keydown', e);
},
},
render() {
const { prefixCls, onKeyDown, tabBarPosition, extraContent } = this;
const { class: className, style, onKeydown, ...restProps } = this.$attrs;
const cls = {
[`${prefixCls}-bar`]: true,
[className]: !!className,
};
const topOrBottom = tabBarPosition === 'top' || tabBarPosition === 'bottom';
const tabBarExtraContentStyle = topOrBottom ? { float: 'right' } : {};
const children = getSlot(this);
let newChildren = children;
if (extraContent) {
newChildren = [
cloneElement(extraContent, {
key: 'extra',
style: {
...tabBarExtraContentStyle,
},
}),
cloneElement(children, { key: 'content' }),
];
newChildren = topOrBottom ? newChildren : newChildren.reverse();
}
return (
<div
role="tablist"
class={cls}
tabindex="0"
onKeydown={onKeyDown}
style={style}
ref={this.saveRef('root')}
{...getDataAttr(restProps)}
>
{newChildren}
</div>
);
},
};

View File

@ -1,83 +0,0 @@
import warning from 'warning';
import PropTypes from '../../_util/vue-types';
import BaseMixin from '../../_util/BaseMixin';
import { getComponent, getPropsData } from '../../_util/props-util';
import { isVertical } from './utils';
function noop() {}
export default {
name: 'TabBarTabsNode',
mixins: [BaseMixin],
inheritAttrs: false,
props: {
activeKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
panels: PropTypes.any.def([]),
prefixCls: PropTypes.string.def(''),
tabBarGutter: PropTypes.any.def(null),
onTabClick: PropTypes.func,
saveRef: PropTypes.func.def(noop),
getRef: PropTypes.func.def(noop),
renderTabBarNode: PropTypes.func,
tabBarPosition: PropTypes.string,
direction: PropTypes.string,
},
render() {
const {
panels: children,
activeKey,
prefixCls,
tabBarGutter,
saveRef,
tabBarPosition,
direction,
} = this.$props;
const rst = [];
const renderTabBarNode = this.renderTabBarNode || this.$slots.renderTabBarNode;
children.forEach((child, index) => {
if (!child) {
return;
}
const props = getPropsData(child);
const key = child.key;
let cls = activeKey === key ? `${prefixCls}-tab-active` : '';
cls += ` ${prefixCls}-tab`;
const events = {};
const disabled = props.disabled;
if (disabled) {
cls += ` ${prefixCls}-tab-disabled`;
} else {
events.onClick = () => {
this.__emit('tabClick', key);
};
}
const tab = getComponent(child, 'tab');
let gutter = tabBarGutter && index === children.length - 1 ? 0 : tabBarGutter;
gutter = typeof gutter === 'number' ? `${gutter}px` : gutter;
const marginProperty = direction === 'rtl' ? 'marginLeft' : 'marginRight';
const style = {
[isVertical(tabBarPosition) ? 'marginBottom' : marginProperty]: gutter,
};
warning(tab !== undefined, 'There must be `tab` property or slot on children of Tabs.');
let node = (
<div
role="tab"
aria-disabled={disabled ? 'true' : 'false'}
aria-selected={activeKey === key ? 'true' : 'false'}
{...events}
class={cls.trim()}
key={key}
style={style}
ref={activeKey === key ? saveRef('activeTab') : noop}
>
{tab}
</div>
);
if (renderTabBarNode) {
node = renderTabBarNode(node);
}
rst.push(node);
});
return <div ref={this.saveRef('navTabsContainer')}>{rst}</div>;
},
};

View File

@ -1,92 +0,0 @@
import PropTypes from '../../_util/vue-types';
import { cloneElement } from '../../_util/vnode';
import {
getTransformByIndex,
getActiveIndex,
getTransformPropValue,
getMarginStyle,
} from './utils';
import { defineComponent } from 'vue';
export default defineComponent({
name: 'TabContent',
inheritAttrs: false,
props: {
animated: PropTypes.looseBool.def(true),
animatedWithMargin: PropTypes.looseBool.def(true),
prefixCls: PropTypes.string.def('ant-tabs'),
activeKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
tabBarPosition: PropTypes.string,
direction: PropTypes.string,
destroyInactiveTabPane: PropTypes.looseBool,
children: PropTypes.any,
},
computed: {
classes() {
const { animated, prefixCls } = this;
const { class: className } = this.$attrs;
return {
[className]: !!className,
[`${prefixCls}-content`]: true,
[animated ? `${prefixCls}-content-animated` : `${prefixCls}-content-no-animated`]: true,
};
},
},
methods: {
getTabPanes(children) {
const props = this.$props;
const activeKey = props.activeKey;
const newChildren = [];
children.forEach(child => {
if (!child) {
return;
}
const key = child.key;
const active = activeKey === key;
newChildren.push(
cloneElement(child, {
active,
destroyInactiveTabPane: props.destroyInactiveTabPane,
rootPrefixCls: props.prefixCls,
}),
);
});
return newChildren;
},
},
render() {
const {
activeKey,
tabBarPosition,
animated,
animatedWithMargin,
direction,
classes,
children,
} = this;
let style = {};
if (animated && children) {
const activeIndex = getActiveIndex(children, activeKey);
if (activeIndex !== -1) {
const animatedStyle = animatedWithMargin
? getMarginStyle(activeIndex, tabBarPosition)
: getTransformPropValue(getTransformByIndex(activeIndex, tabBarPosition, direction));
style = {
...this.$attrs.style,
...animatedStyle,
};
} else {
style = {
...this.$attrs.style,
display: 'none',
};
}
}
return (
<div class={classes} style={style}>
{this.getTabPanes(children || [])}
</div>
);
},
});

View File

@ -1,53 +0,0 @@
import { defineComponent, inject } from 'vue';
import PropTypes from '../../_util/vue-types';
import { getComponent, getSlot } from '../../_util/props-util';
import Sentinel from './Sentinel';
export default defineComponent({
name: 'TabPane',
props: {
active: PropTypes.looseBool,
destroyInactiveTabPane: PropTypes.looseBool,
forceRender: PropTypes.looseBool,
placeholder: PropTypes.any,
rootPrefixCls: PropTypes.string,
tab: PropTypes.any,
closable: PropTypes.looseBool,
disabled: PropTypes.looseBool,
},
setup() {
return {
isActived: undefined,
sentinelContext: inject('sentinelContext', {}),
};
},
render() {
const { destroyInactiveTabPane, active, forceRender, rootPrefixCls } = this.$props;
const children = getSlot(this);
const placeholder = getComponent(this, 'placeholder');
this.isActived = this.isActived || active;
const prefixCls = `${rootPrefixCls}-tabpane`;
const cls = {
[prefixCls]: 1,
[`${prefixCls}-inactive`]: !active,
[`${prefixCls}-active`]: active,
};
const isRender = destroyInactiveTabPane ? active : this.isActived;
const shouldRender = isRender || forceRender;
const { sentinelStart, sentinelEnd, setPanelSentinelStart, setPanelSentinelEnd } =
this.sentinelContext;
let panelSentinelStart;
let panelSentinelEnd;
if (active && shouldRender) {
panelSentinelStart = <Sentinel setRef={setPanelSentinelStart} prevElement={sentinelStart} />;
panelSentinelEnd = <Sentinel setRef={setPanelSentinelEnd} nextElement={sentinelEnd} />;
}
return (
<div class={cls} role="tabpanel" aria-hidden={active ? 'false' : 'true'}>
{panelSentinelStart}
{shouldRender ? children : placeholder}
{panelSentinelEnd}
</div>
);
},
});

View File

@ -1,254 +0,0 @@
import { defineComponent, provide, reactive, watchEffect } from 'vue';
import BaseMixin from '../../_util/BaseMixin';
import PropTypes from '../../_util/vue-types';
import KeyCode from './KeyCode';
import { cloneElement } from '../../_util/vnode';
import Sentinel from './Sentinel';
import isValid from '../../_util/isValid';
import { getDataAttr } from './utils';
function getDefaultActiveKey(props) {
let activeKey;
const children = props.children;
children.forEach(child => {
if (child && !isValid(activeKey) && !child.disabled) {
activeKey = child.key;
}
});
return activeKey;
}
function activeKeyIsValid(props, key) {
const children = props.children;
const keys = children.map(child => child && child.key);
return keys.indexOf(key) >= 0;
}
export default defineComponent({
name: 'Tabs',
mixins: [BaseMixin],
inheritAttrs: false,
props: {
destroyInactiveTabPane: PropTypes.looseBool,
renderTabBar: PropTypes.func.isRequired,
renderTabContent: PropTypes.func.isRequired,
navWrapper: PropTypes.func.def(arg => arg),
children: PropTypes.any.def([]),
prefixCls: PropTypes.string.def('ant-tabs'),
tabBarPosition: PropTypes.string.def('top'),
activeKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
defaultActiveKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
direction: PropTypes.string.def('ltr'),
tabBarGutter: PropTypes.number,
},
setup(props) {
let activeKey;
if (props.activeKey !== undefined) {
// eslint-disable-next-line vue/no-setup-props-destructure
activeKey = props.activeKey;
} else if (props.defaultActiveKey !== undefined) {
// eslint-disable-next-line vue/no-setup-props-destructure
activeKey = props.defaultActiveKey;
} else {
activeKey = getDefaultActiveKey(props);
}
const state = reactive({
_activeKey: activeKey,
});
watchEffect(
() => {
if (props.activeKey !== undefined) {
state._activeKey = props.activeKey;
} else if (!activeKeyIsValid(props, state._activeKey)) {
// https://github.com/ant-design/ant-design/issues/7093
state._activeKey = getDefaultActiveKey(props);
}
},
{
flush: 'sync',
},
);
return { state };
},
created() {
this.panelSentinelStart = undefined;
this.panelSentinelEnd = undefined;
this.sentinelStart = undefined;
this.sentinelEnd = undefined;
provide('sentinelContext', this);
},
beforeUnmount() {
this.destroy = true;
cancelAnimationFrame(this.sentinelId);
},
methods: {
onTabClick(activeKey, e) {
if (this.tabBar.props && this.tabBar.props.onTabClick) {
this.tabBar.props.onTabClick(activeKey, e);
}
this.setActiveKey(activeKey);
},
onNavKeyDown(e) {
const eventKeyCode = e.keyCode;
if (eventKeyCode === KeyCode.RIGHT || eventKeyCode === KeyCode.DOWN) {
e.preventDefault();
const nextKey = this.getNextActiveKey(true);
this.onTabClick(nextKey);
} else if (eventKeyCode === KeyCode.LEFT || eventKeyCode === KeyCode.UP) {
e.preventDefault();
const previousKey = this.getNextActiveKey(false);
this.onTabClick(previousKey);
}
},
onScroll({ target, currentTarget }) {
if (target === currentTarget && target.scrollLeft > 0) {
target.scrollLeft = 0;
}
},
// Sentinel for tab index
setSentinelStart(node) {
this.sentinelStart = node;
},
setSentinelEnd(node) {
this.sentinelEnd = node;
},
setPanelSentinelStart(node) {
if (node !== this.panelSentinelStart) {
this.updateSentinelContext();
}
this.panelSentinelStart = node;
},
setPanelSentinelEnd(node) {
if (node !== this.panelSentinelEnd) {
this.updateSentinelContext();
}
this.panelSentinelEnd = node;
},
setActiveKey(activeKey) {
if (this.state._activeKey !== activeKey) {
const props = this.$props;
if (props.activeKey === undefined) {
this.state._activeKey = activeKey;
}
this.__emit('update:activeKey', activeKey);
this.__emit('change', activeKey);
}
},
getNextActiveKey(next) {
const activeKey = this.state._activeKey;
const children = [];
this.$props.children.forEach(c => {
if (c && !c.props?.disabled && c.props?.disabled !== '') {
if (next) {
children.push(c);
} else {
children.unshift(c);
}
}
});
const length = children.length;
let ret = length && children[0].key;
children.forEach((child, i) => {
if (child.key === activeKey) {
if (i === length - 1) {
ret = children[0].key;
} else {
ret = children[i + 1].key;
}
}
});
return ret;
},
updateSentinelContext() {
if (this.destroy) return;
cancelAnimationFrame(this.sentinelId);
this.sentinelId = requestAnimationFrame(() => {
if (this.destroy) return;
this.$forceUpdate();
});
},
},
render() {
const props = this.$props;
const {
prefixCls,
navWrapper,
tabBarPosition,
renderTabContent,
renderTabBar,
destroyInactiveTabPane,
direction,
tabBarGutter,
} = props;
const { class: className, onChange, style, ...restProps } = this.$attrs;
const cls = {
[className]: className,
[prefixCls]: 1,
[`${prefixCls}-${tabBarPosition}`]: 1,
[`${prefixCls}-rtl`]: direction === 'rtl',
};
this.tabBar = renderTabBar();
const tabBar = cloneElement(this.tabBar, {
prefixCls,
navWrapper,
tabBarPosition,
panels: props.children,
activeKey: this.state._activeKey,
direction,
tabBarGutter,
onKeydown: this.onNavKeyDown,
onTabClick: this.onTabClick,
key: 'tabBar',
});
const tabContent = cloneElement(renderTabContent(), {
prefixCls,
tabBarPosition,
activeKey: this.state._activeKey,
destroyInactiveTabPane,
direction,
onChange: this.setActiveKey,
children: props.children,
key: 'tabContent',
});
const sentinelStart = (
<Sentinel
key="sentinelStart"
setRef={this.setSentinelStart}
nextElement={this.panelSentinelStart}
/>
);
const sentinelEnd = (
<Sentinel
key="sentinelEnd"
setRef={this.setSentinelEnd}
prevElement={this.panelSentinelEnd}
/>
);
const contents = [];
if (tabBarPosition === 'bottom') {
contents.push(sentinelStart, tabContent, sentinelEnd, tabBar);
} else {
contents.push(tabBar, sentinelStart, tabContent, sentinelEnd);
}
const p = {
...getDataAttr(restProps),
style,
onScroll: this.onScroll,
class: cls,
};
return <div {...p}>{contents}</div>;
},
});

View File

@ -1,7 +0,0 @@
// based on rc-tabs 9.7.0
import Tabs from './Tabs';
import TabPane from './TabPane';
import TabContent from './TabContent';
export default Tabs;
export { TabPane, TabContent };

View File

@ -1,132 +0,0 @@
import { isVNode } from 'vue';
export function toArray(children) {
const c = [];
children.forEach(child => {
if (isVNode(child)) {
c.push(child);
}
});
return c;
}
export function getActiveIndex(children, activeKey) {
const c = toArray(children);
for (let i = 0; i < c.length; i++) {
if (c[i].key === activeKey) {
return i;
}
}
return -1;
}
export function getActiveKey(children, index) {
const c = toArray(children);
return c[index].key;
}
export function setTransform(style, v) {
style.transform = v;
style.webkitTransform = v;
style.mozTransform = v;
}
export function isTransform3dSupported(style) {
return (
('transform' in style || 'webkitTransform' in style || 'MozTransform' in style) && window.atob
);
}
export function setTransition(style, v) {
style.transition = v;
style.webkitTransition = v;
style.MozTransition = v;
}
export function getTransformPropValue(v) {
return {
transform: v,
WebkitTransform: v,
MozTransform: v,
};
}
export function isVertical(tabBarPosition) {
return tabBarPosition === 'left' || tabBarPosition === 'right';
}
export function getTransformByIndex(index, tabBarPosition, direction = 'ltr') {
const translate = isVertical(tabBarPosition) ? 'translateY' : 'translateX';
if (!isVertical(tabBarPosition) && direction === 'rtl') {
return `${translate}(${index * 100}%) translateZ(0)`;
}
return `${translate}(${-index * 100}%) translateZ(0)`;
}
export function getMarginStyle(index, tabBarPosition) {
const marginDirection = isVertical(tabBarPosition) ? 'marginTop' : 'marginLeft';
return {
[marginDirection]: `${-index * 100}%`,
};
}
export function getStyle(el, property) {
return +window.getComputedStyle(el).getPropertyValue(property).replace('px', '');
}
export function setPxStyle(el, value, vertical) {
value = vertical ? `0px, ${value}px, 0px` : `${value}px, 0px, 0px`;
setTransform(el.style, `translate3d(${value})`);
}
export function getDataAttr(props) {
return Object.keys(props).reduce((prev, key) => {
if (key.substr(0, 5) === 'aria-' || key.substr(0, 5) === 'data-' || key === 'role') {
prev[key] = props[key];
}
return prev;
}, {});
}
function toNum(style, property) {
return +style.getPropertyValue(property).replace('px', '');
}
function getTypeValue(start, current, end, tabNode, wrapperNode) {
let total = getStyle(wrapperNode, `padding-${start}`);
if (!tabNode || !tabNode.parentNode) {
return total;
}
const { childNodes } = tabNode.parentNode;
Array.prototype.some.call(childNodes, node => {
if (!node.tagName) {
return false;
}
const style = window.getComputedStyle(node);
if (node !== tabNode) {
total += toNum(style, `margin-${start}`);
total += node[current];
total += toNum(style, `margin-${end}`);
if (style.boxSizing === 'content-box') {
total += toNum(style, `border-${start}-width`) + toNum(style, `border-${end}-width`);
}
return false;
}
// We need count current node margin
// ref: https://github.com/react-component/tabs/pull/139#issuecomment-431005262
total += toNum(style, `margin-${start}`);
return true;
});
return total;
}
export function getLeft(tabNode, wrapperNode) {
return getTypeValue('left', 'offsetWidth', 'right', tabNode, wrapperNode);
}
export function getTop(tabNode, wrapperNode) {
return getTypeValue('top', 'offsetHeight', 'bottom', tabNode, wrapperNode);
}

View File

@ -1,5 +1,5 @@
// debugger tsx
import Demo from '../../components/auto-complete/demo/index.vue';
import Demo from '../../components/tabs/demo/basic.vue';
export default {
render() {