refactor: tabs & card (#4732)

* refactor: tabs

* refactor: tabs

* fix: tabs hotreload error

* refactor: rename useRef

* feat: add leftExtra rightExtra

* refactor: tabs

* test: update tabs test

* refactor: card

* doc: update tabs demo

* refactor: add card style

* style: update vue dep
pull/4738/head
tangjinzhou 2021-10-07 09:23:36 +08:00 committed by GitHub
parent 022a3ce795
commit 75cf264040
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 3797 additions and 3320 deletions

View File

@ -1,17 +0,0 @@
import type { Ref } from 'vue';
import { onBeforeUpdate, ref } from 'vue';
export type UseRef = [(el: any, key: string | number) => void, Ref<any>];
export type Refs = Record<string | number, any>;
export const useRef = (): UseRef => {
const refs = ref<Refs>({});
const setRef = (el: any, key: string | number) => {
refs.value[key] = el;
};
onBeforeUpdate(() => {
refs.value = {};
});
return [setRef, refs];
};
export default useRef;

View File

@ -0,0 +1,20 @@
import type { Ref, ComponentPublicInstance } from 'vue';
import { onBeforeUpdate, ref } from 'vue';
import type { Key } from '../type';
type RefType = HTMLElement | ComponentPublicInstance;
export type RefsValue = Map<Key, RefType>;
type UseRef = [(key: Key) => (el: RefType) => void, Ref<RefsValue>];
const useRefs = (): UseRef => {
const refs = ref<RefsValue>(new Map());
const setRef = (key: Key) => (el: RefType) => {
refs.value.set(key, el);
};
onBeforeUpdate(() => {
refs.value = new Map();
});
return [setRef, refs];
};
export default useRefs;

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

@ -1,68 +1,62 @@
import type { VNodeTypes, PropType, VNode, ExtractPropTypes } from 'vue';
import { inject, isVNode, defineComponent } from 'vue';
import { tuple } from '../_util/type';
import { isVNode, defineComponent, renderSlot } from 'vue';
import Tabs from '../tabs';
import Row from '../row';
import Col from '../col';
import PropTypes from '../_util/vue-types';
import { getComponent, getSlot, isEmptyElement } from '../_util/props-util';
import { flattenChildren, isEmptyElement } from '../_util/props-util';
import BaseMixin from '../_util/BaseMixin';
import { defaultConfigProvider } from '../config-provider';
import type { SizeType } from '../config-provider';
import isPlainObject from 'lodash-es/isPlainObject';
import useConfigInject from '../_util/hooks/useConfigInject';
import devWarning from '../vc-util/devWarning';
export interface CardTabListType {
key: string;
tab: VNodeTypes;
tab: any;
/** @deprecated Please use `customTab` instead. */
slots?: { tab: string };
disabled?: boolean;
}
export type CardType = 'inner';
export type CardSize = 'default' | 'small';
const { TabPane } = Tabs;
const cardProps = {
const cardProps = () => ({
prefixCls: PropTypes.string,
title: PropTypes.VNodeChild,
extra: PropTypes.VNodeChild,
title: PropTypes.any,
extra: PropTypes.any,
bordered: PropTypes.looseBool.def(true),
bodyStyle: PropTypes.style,
headStyle: PropTypes.style,
loading: PropTypes.looseBool.def(false),
hoverable: PropTypes.looseBool.def(false),
type: PropTypes.string,
size: PropTypes.oneOf(tuple('default', 'small')),
actions: PropTypes.VNodeChild,
type: { type: String as PropType<CardType> },
size: { type: String as PropType<CardSize> },
actions: PropTypes.any,
tabList: {
type: Array as PropType<CardTabListType[]>,
},
tabBarExtraContent: PropTypes.VNodeChild,
tabBarExtraContent: PropTypes.any,
activeTabKey: PropTypes.string,
defaultActiveTabKey: PropTypes.string,
cover: PropTypes.VNodeChild,
cover: PropTypes.any,
onTabChange: {
type: Function as PropType<(key: string) => void>,
},
};
});
export type CardProps = Partial<ExtractPropTypes<typeof cardProps>>;
export type CardProps = Partial<ExtractPropTypes<ReturnType<typeof cardProps>>>;
const Card = defineComponent({
name: 'ACard',
mixins: [BaseMixin],
props: cardProps,
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
};
},
data() {
return {
widerPadding: false,
};
},
methods: {
getAction(actions: VNodeTypes[]) {
props: cardProps(),
slots: ['title', 'extra', 'tabBarExtraContent', 'actions', 'cover', 'customTab'],
setup(props, { slots }) {
const { prefixCls, direction, size } = useConfigInject('card', props);
const getAction = (actions: VNodeTypes[]) => {
const actionList = actions.map((action, index) =>
(isVNode(action) && !isEmptyElement(action)) || !isVNode(action) ? (
<li style={{ width: `${100 / actions.length}%` }} key={`action-${index}`}>
@ -71,11 +65,11 @@ const Card = defineComponent({
) : null,
);
return actionList;
},
triggerTabChange(key: string) {
this.$emit('tabChange', key);
},
isContainGrid(obj: VNode[] = []) {
};
const triggerTabChange = (key: string) => {
props.onTabChange?.(key);
};
const isContainGrid = (obj: VNode[] = []) => {
let containGrid: boolean;
obj.forEach(element => {
if (element && isPlainObject(element.type) && (element.type as any).__ANT_CARD_GRID) {
@ -83,145 +77,129 @@ const Card = defineComponent({
}
});
return containGrid;
},
},
render() {
const {
prefixCls: customizePrefixCls,
headStyle = {},
bodyStyle = {},
loading,
bordered = true,
size = 'default',
type,
tabList,
hoverable,
activeTabKey,
defaultActiveTabKey,
} = this.$props;
const { $slots } = this;
const children = getSlot(this);
const { getPrefixCls } = this.configProvider;
const prefixCls = getPrefixCls('card', customizePrefixCls);
const tabBarExtraContent = getComponent(this, 'tabBarExtraContent');
const classString = {
[`${prefixCls}`]: true,
[`${prefixCls}-loading`]: loading,
[`${prefixCls}-bordered`]: bordered,
[`${prefixCls}-hoverable`]: !!hoverable,
[`${prefixCls}-contain-grid`]: this.isContainGrid(children),
[`${prefixCls}-contain-tabs`]: tabList && tabList.length,
[`${prefixCls}-${size}`]: size !== 'default',
[`${prefixCls}-type-${type}`]: !!type,
};
const loadingBlockStyle =
bodyStyle.padding === 0 || bodyStyle.padding === '0px' ? { padding: 24 } : undefined;
return () => {
const {
headStyle = {},
bodyStyle = {},
loading,
bordered = true,
type,
tabList,
hoverable,
activeTabKey,
defaultActiveTabKey,
tabBarExtraContent = slots.tabBarExtraContent?.(),
title = slots.title?.(),
extra = slots.extra?.(),
actions = slots.actions?.(),
cover = slots.cover?.(),
} = props;
const children = flattenChildren(slots.default?.());
const pre = prefixCls.value;
const classString = {
[`${pre}`]: true,
[`${pre}-loading`]: loading,
[`${pre}-bordered`]: bordered,
[`${pre}-hoverable`]: !!hoverable,
[`${pre}-contain-grid`]: isContainGrid(children),
[`${pre}-contain-tabs`]: tabList && tabList.length,
[`${pre}-${size.value}`]: size.value,
[`${pre}-type-${type}`]: !!type,
[`${pre}-rtl`]: direction.value === 'rtl',
};
const loadingBlock = (
<div class={`${prefixCls}-loading-content`} style={loadingBlockStyle}>
<Row gutter={8}>
<Col span={22}>
<div class={`${prefixCls}-loading-block`} />
</Col>
</Row>
<Row gutter={8}>
<Col span={8}>
<div class={`${prefixCls}-loading-block`} />
</Col>
<Col span={15}>
<div class={`${prefixCls}-loading-block`} />
</Col>
</Row>
<Row gutter={8}>
<Col span={6}>
<div class={`${prefixCls}-loading-block`} />
</Col>
<Col span={18}>
<div class={`${prefixCls}-loading-block`} />
</Col>
</Row>
<Row gutter={8}>
<Col span={13}>
<div class={`${prefixCls}-loading-block`} />
</Col>
<Col span={9}>
<div class={`${prefixCls}-loading-block`} />
</Col>
</Row>
<Row gutter={8}>
<Col span={4}>
<div class={`${prefixCls}-loading-block`} />
</Col>
<Col span={3}>
<div class={`${prefixCls}-loading-block`} />
</Col>
<Col span={16}>
<div class={`${prefixCls}-loading-block`} />
</Col>
</Row>
</div>
);
const loadingBlockStyle =
bodyStyle.padding === 0 || bodyStyle.padding === '0px' ? { padding: '24px' } : undefined;
const hasActiveTabKey = activeTabKey !== undefined;
const tabsProps = {
size: 'large',
[hasActiveTabKey ? 'activeKey' : 'defaultActiveKey']: hasActiveTabKey
? activeTabKey
: defaultActiveTabKey,
tabBarExtraContent,
onChange: this.triggerTabChange,
class: `${prefixCls}-head-tabs`,
};
let head;
const tabs =
tabList && tabList.length ? (
<Tabs {...tabsProps}>
{tabList.map(item => {
const { tab: temp, slots } = item as CardTabListType;
const name = slots?.tab;
const tab = temp !== undefined ? temp : $slots[name] ? $slots[name](item) : null;
return <TabPane tab={tab} key={item.key} disabled={item.disabled} />;
})}
</Tabs>
) : null;
const titleDom = getComponent(this, 'title');
const extraDom = getComponent(this, 'extra');
if (titleDom || extraDom || tabs) {
head = (
<div class={`${prefixCls}-head`} style={headStyle}>
<div class={`${prefixCls}-head-wrapper`}>
{titleDom && <div class={`${prefixCls}-head-title`}>{titleDom}</div>}
{extraDom && <div class={`${prefixCls}-extra`}>{extraDom}</div>}
</div>
{tabs}
const block = <div class={`${pre}-loading-block`} />;
const loadingBlock = (
<div class={`${pre}-loading-content`} style={loadingBlockStyle}>
<Row gutter={8}>
<Col span={22}>{block}</Col>
</Row>
<Row gutter={8}>
<Col span={8}>{block}</Col>
<Col span={15}>{block}</Col>
</Row>
<Row gutter={8}>
<Col span={6}>{block}</Col>
<Col span={18}>{block}</Col>
</Row>
<Row gutter={8}>
<Col span={13}>{block}</Col>
<Col span={9}>{block}</Col>
</Row>
<Row gutter={8}>
<Col span={4}>{block}</Col>
<Col span={3}>{block}</Col>
<Col span={16}>{block}</Col>
</Row>
</div>
);
}
const cover = getComponent(this, 'cover');
const coverDom = cover ? <div class={`${prefixCls}-cover`}>{cover}</div> : null;
const body = (
<div class={`${prefixCls}-body`} style={bodyStyle}>
{loading ? loadingBlock : children}
</div>
);
const actions = getComponent(this, 'actions');
const actionDom =
actions && actions.length ? (
<ul class={`${prefixCls}-actions`}>{this.getAction(actions)}</ul>
) : null;
const hasActiveTabKey = activeTabKey !== undefined;
const tabsProps = {
size: 'large' as SizeType,
[hasActiveTabKey ? 'activeKey' : 'defaultActiveKey']: hasActiveTabKey
? activeTabKey
: defaultActiveTabKey,
onChange: triggerTabChange,
class: `${pre}-head-tabs`,
};
return (
<div class={classString} ref="cardContainerRef">
{head}
{coverDom}
{children ? body : null}
{actionDom}
</div>
);
let head;
const tabs =
tabList && tabList.length ? (
<Tabs
{...tabsProps}
v-slots={{ rightExtra: tabBarExtraContent ? () => tabBarExtraContent : null }}
>
{tabList.map(item => {
const { tab: temp, slots: itemSlots } = item as CardTabListType;
const name = itemSlots?.tab;
devWarning(
!itemSlots,
'Card',
`tabList slots is deprecated, Please use \`customTab\` instead.`,
);
let tab = temp !== undefined ? temp : slots[name] ? slots[name](item) : null;
tab = renderSlot(slots, 'customTab', item as any, () => [tab]);
return <TabPane tab={tab} key={item.key} disabled={item.disabled} />;
})}
</Tabs>
) : null;
if (title || extra || tabs) {
head = (
<div class={`${pre}-head`} style={headStyle}>
<div class={`${pre}-head-wrapper`}>
{title && <div class={`${pre}-head-title`}>{title}</div>}
{extra && <div class={`${pre}-extra`}>{extra}</div>}
</div>
{tabs}
</div>
);
}
const coverDom = cover ? <div class={`${pre}-cover`}>{cover}</div> : null;
const body = (
<div class={`${pre}-body`} style={bodyStyle}>
{loading ? loadingBlock : children}
</div>
);
const actionDom =
actions && actions.length ? <ul class={`${pre}-actions`}>{getAction(actions)}</ul> : null;
return (
<div class={classString} ref="cardContainerRef">
{head}
{coverDom}
{children && children.length ? body : null}
{actionDom}
</div>
);
};
},
});

View File

@ -1,30 +1,23 @@
import { defineComponent, inject } from 'vue';
import PropTypes from '../_util/vue-types';
import { defaultConfigProvider } from '../config-provider';
import { getSlot } from '../_util/props-util';
import { defineComponent, computed } from 'vue';
import useConfigInject from '../_util/hooks/useConfigInject';
export default defineComponent({
name: 'ACardGrid',
__ANT_CARD_GRID: true,
props: {
prefixCls: PropTypes.string,
hoverable: PropTypes.looseBool,
prefixCls: String,
hoverable: { type: Boolean, default: true },
},
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
setup(props, { slots }) {
const { prefixCls } = useConfigInject('card', props);
const classNames = computed(() => {
return {
[`${prefixCls.value}-grid`]: true,
[`${prefixCls.value}-grid-hoverable`]: props.hoverable,
};
});
return () => {
return <div class={classNames.value}>{slots.default?.()}</div>;
};
},
render() {
const { prefixCls: customizePrefixCls, hoverable = true } = this.$props;
const { getPrefixCls } = this.configProvider;
const prefixCls = getPrefixCls('card', customizePrefixCls);
const classString = {
[`${prefixCls}-grid`]: true,
[`${prefixCls}-grid-hoverable`]: hoverable,
};
return <div class={classString}>{getSlot(this)}</div>;
},
});

View File

@ -1,52 +1,47 @@
import { defineComponent, inject } from 'vue';
import { defineComponent } from 'vue';
import PropTypes from '../_util/vue-types';
import { getComponent } from '../_util/props-util';
import { defaultConfigProvider } from '../config-provider';
import { getPropsSlot } from '../_util/props-util';
import useConfigInject from '../_util/hooks/useConfigInject';
export default defineComponent({
name: 'ACardMeta',
props: {
prefixCls: PropTypes.string,
title: PropTypes.VNodeChild,
description: PropTypes.VNodeChild,
avatar: PropTypes.VNodeChild,
title: PropTypes.any,
description: PropTypes.any,
avatar: PropTypes.any,
},
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
};
},
render() {
const { prefixCls: customizePrefixCls } = this.$props;
slots: ['title', 'description', 'avatar'],
setup(props, { slots }) {
const { prefixCls } = useConfigInject('card', props);
return () => {
const classString = {
[`${prefixCls.value}-meta`]: true,
};
const avatar = getPropsSlot(slots, props, 'avatar');
const title = getPropsSlot(slots, props, 'title');
const description = getPropsSlot(slots, props, 'description');
const { getPrefixCls } = this.configProvider;
const prefixCls = getPrefixCls('card', customizePrefixCls);
const classString = {
[`${prefixCls}-meta`]: true,
};
const avatar = getComponent(this, 'avatar');
const title = getComponent(this, 'title');
const description = getComponent(this, 'description');
const avatarDom = avatar ? <div class={`${prefixCls}-meta-avatar`}>{avatar}</div> : null;
const titleDom = title ? <div class={`${prefixCls}-meta-title`}>{title}</div> : null;
const descriptionDom = description ? (
<div class={`${prefixCls}-meta-description`}>{description}</div>
) : null;
const MetaDetail =
titleDom || descriptionDom ? (
<div class={`${prefixCls}-meta-detail`}>
{titleDom}
{descriptionDom}
</div>
const avatarDom = avatar ? (
<div class={`${prefixCls.value}-meta-avatar`}>{avatar}</div>
) : null;
return (
<div class={classString}>
{avatarDom}
{MetaDetail}
</div>
);
const titleDom = title ? <div class={`${prefixCls.value}-meta-title`}>{title}</div> : null;
const descriptionDom = description ? (
<div class={`${prefixCls.value}-meta-description`}>{description}</div>
) : null;
const MetaDetail =
titleDom || descriptionDom ? (
<div class={`${prefixCls.value}-meta-detail`}>
{titleDom}
{descriptionDom}
</div>
) : null;
return (
<div class={classString}>
{avatarDom}
{MetaDetail}
</div>
);
};
},
});

View File

@ -294,35 +294,40 @@ exports[`renders ./components/card/demo/tabs.vue correctly 1`] = `
<div class="ant-card-head-title">Card title</div>
<div class="ant-card-extra"><a href="#">More</a></div>
</div>
<div class="ant-card-head-tabs ant-tabs-large ant-tabs-line ant-tabs ant-tabs-top">
<div role="tablist" class="ant-tabs-bar ant-tabs-top-bar ant-tabs-large-bar" tabindex="0">
<div class="ant-tabs-nav-container"><span unselectable="unselectable" class="ant-tabs-tab-prev ant-tabs-tab-btn-disabled"><span class="ant-tabs-tab-prev-icon"><span role="img" aria-label="left" class="anticon anticon-left ant-tabs-tab-prev-icon-target"><svg focusable="false" class="" data-icon="left" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg></span></span></span><span unselectable="unselectable" class="ant-tabs-tab-next ant-tabs-tab-btn-disabled"><span class="ant-tabs-tab-next-icon"><span role="img" aria-label="right" class="anticon anticon-right ant-tabs-tab-next-icon-target"><svg focusable="false" class="" data-icon="right" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"></path></svg></span></span></span>
<div class="ant-tabs-nav-wrap">
<div class="ant-tabs-nav-scroll">
<div class="ant-tabs-nav ant-tabs-nav-animated">
<div>
<div role="tab" aria-disabled="false" aria-selected="true" class="ant-tabs-tab-active ant-tabs-tab"><span><span role="img" aria-label="home" class="anticon anticon-home"><svg focusable="false" class="" data-icon="home" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M946.5 505L560.1 118.8l-25.9-25.9a31.5 31.5 0 00-44.4 0L77.5 505a63.9 63.9 0 00-18.8 46c.4 35.2 29.7 63.3 64.9 63.3h42.5V940h691.8V614.3h43.4c17.1 0 33.2-6.7 45.3-18.8a63.6 63.6 0 0018.7-45.3c0-17-6.7-33.1-18.8-45.2zM568 868H456V664h112v204zm217.9-325.7V868H632V640c0-22.1-17.9-40-40-40H432c-22.1 0-40 17.9-40 40v228H238.1V542.3h-96l370-369.7 23.1 23.1L882 542.3h-96.1z"></path></svg></span> tab1</span></div>
<div role="tab" aria-disabled="false" aria-selected="false" class="ant-tabs-tab">tab2</div>
</div>
<div class="ant-tabs-ink-bar ant-tabs-ink-bar-animated" style="display: block; transform: translate3d(0px,0,0); webkit-transform: translate3d(0px,0,0); width: 0px;"></div>
</div>
<div class="ant-tabs ant-tabs-top ant-tabs-large ant-card-head-tabs">
<div role="tablist" class="ant-tabs-nav">
<!---->
<div class="ant-tabs-nav-wrap">
<div class="ant-tabs-nav-list" style="transform: translate(0px, 0px);">
<div class="ant-tabs-tab ant-tabs-tab-active">
<div role="tab" aria-selected="true" class="ant-tabs-tab-btn" tabindex="0" id="rc-tabs-test-tab-tab1" aria-controls="rc-tabs-test-panel-tab1"><span><span role="img" aria-label="home" class="anticon anticon-home"><svg focusable="false" class="" data-icon="home" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M946.5 505L560.1 118.8l-25.9-25.9a31.5 31.5 0 00-44.4 0L77.5 505a63.9 63.9 0 00-18.8 46c.4 35.2 29.7 63.3 64.9 63.3h42.5V940h691.8V614.3h43.4c17.1 0 33.2-6.7 45.3-18.8a63.6 63.6 0 0018.7-45.3c0-17-6.7-33.1-18.8-45.2zM568 868H456V664h112v204zm217.9-325.7V868H632V640c0-22.1-17.9-40-40-40H432c-22.1 0-40 17.9-40 40v228H238.1V542.3h-96l370-369.7 23.1 23.1L882 542.3h-96.1z"></path></svg></span> tab1</span></div>
<!---->
</div>
<div class="ant-tabs-tab">
<div role="tab" aria-selected="false" class="ant-tabs-tab-btn" tabindex="0" id="rc-tabs-test-tab-tab2" aria-controls="rc-tabs-test-panel-tab2">tab2</div>
<!---->
</div>
<!---->
<div class="ant-tabs-ink-bar ant-tabs-ink-bar-animated"></div>
</div>
</div>
<div class="ant-tabs-nav-operations ant-tabs-nav-operations-hidden">
<!----><button type="button" class="ant-tabs-nav-more" style="visibility: hidden; order: 1;" tabindex="-1" aria-hidden="true" aria-haspopup="listbox" aria-controls="rc-tabs-test-more-popup" id="rc-tabs-test-more" aria-expanded="false"><span role="img" aria-label="ellipsis" class="anticon anticon-ellipsis"><svg focusable="false" class="" data-icon="ellipsis" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"></path></svg></span></button>
<!---->
</div>
<!---->
<!---->
</div>
<div class="ant-tabs-content-holder">
<div class="ant-tabs-content ant-tabs-content-top">
<div role="tabpanel" tabindex="0" aria-hidden="false" class="ant-tabs-tabpane ant-tabs-tabpane-active" id="rc-tabs-test-panel-tab1" aria-labelledby="rc-tabs-test-tab-tab1">
<!---->
</div>
<div role="tabpanel" tabindex="-1" aria-hidden="true" style="display: none;" class="ant-tabs-tabpane" id="rc-tabs-test-panel-tab2" aria-labelledby="rc-tabs-test-tab-tab2">
<!---->
</div>
</div>
</div>
<div tabindex="0" style="width: 0px; height: 0px; overflow: hidden; position: absolute;" role="presentation"></div>
<div class="ant-tabs-top-content ant-tabs-content ant-tabs-content-animated" style="margin-left: 0%;">
<div class="ant-tabs-tabpane ant-tabs-tabpane-active" role="tabpanel" aria-hidden="false">
<div tabindex="0" style="width: 0px; height: 0px; overflow: hidden; position: absolute;" role="presentation"></div>
<div tabindex="0" style="width: 0px; height: 0px; overflow: hidden; position: absolute;" role="presentation"></div>
</div>
<div class="ant-tabs-tabpane ant-tabs-tabpane-inactive" role="tabpanel" aria-hidden="true">
<!---->
<!---->
<!---->
</div>
</div>
<div tabindex="0" style="width: 0px; height: 0px; overflow: hidden; position: absolute;" role="presentation"></div>
</div>
</div>
<!---->
@ -335,42 +340,47 @@ exports[`renders ./components/card/demo/tabs.vue correctly 1`] = `
<!---->
<!---->
</div>
<div class="ant-card-head-tabs ant-tabs-large ant-tabs-line ant-tabs ant-tabs-top">
<div role="tablist" class="ant-tabs-bar ant-tabs-top-bar ant-tabs-large-bar" tabindex="0">
<div class="ant-tabs-extra-content" style="float: right;"><a href="#">More</a></div>
<div class="ant-tabs-nav-container"><span unselectable="unselectable" class="ant-tabs-tab-prev ant-tabs-tab-btn-disabled"><span class="ant-tabs-tab-prev-icon"><span role="img" aria-label="left" class="anticon anticon-left ant-tabs-tab-prev-icon-target"><svg focusable="false" class="" data-icon="left" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg></span></span></span><span unselectable="unselectable" class="ant-tabs-tab-next ant-tabs-tab-btn-disabled"><span class="ant-tabs-tab-next-icon"><span role="img" aria-label="right" class="anticon anticon-right ant-tabs-tab-next-icon-target"><svg focusable="false" class="" data-icon="right" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"></path></svg></span></span></span>
<div class="ant-tabs-nav-wrap">
<div class="ant-tabs-nav-scroll">
<div class="ant-tabs-nav ant-tabs-nav-animated">
<div>
<div role="tab" aria-disabled="false" aria-selected="false" class="ant-tabs-tab">article</div>
<div role="tab" aria-disabled="false" aria-selected="true" class="ant-tabs-tab-active ant-tabs-tab">app</div>
<div role="tab" aria-disabled="false" aria-selected="false" class="ant-tabs-tab">project</div>
</div>
<div class="ant-tabs-ink-bar ant-tabs-ink-bar-animated" style="display: block; transform: translate3d(0px,0,0); webkit-transform: translate3d(0px,0,0); width: 0px;"></div>
</div>
<div class="ant-tabs ant-tabs-top ant-tabs-large ant-card-head-tabs">
<div role="tablist" class="ant-tabs-nav">
<!---->
<div class="ant-tabs-nav-wrap">
<div class="ant-tabs-nav-list" style="transform: translate(0px, 0px);">
<div class="ant-tabs-tab">
<div role="tab" aria-selected="false" class="ant-tabs-tab-btn" tabindex="0" id="rc-tabs-test-tab-article" aria-controls="rc-tabs-test-panel-article">article</div>
<!---->
</div>
<div class="ant-tabs-tab ant-tabs-tab-active">
<div role="tab" aria-selected="true" class="ant-tabs-tab-btn" tabindex="0" id="rc-tabs-test-tab-app" aria-controls="rc-tabs-test-panel-app">app</div>
<!---->
</div>
<div class="ant-tabs-tab">
<div role="tab" aria-selected="false" class="ant-tabs-tab-btn" tabindex="0" id="rc-tabs-test-tab-project" aria-controls="rc-tabs-test-panel-project">project</div>
<!---->
</div>
<!---->
<div class="ant-tabs-ink-bar ant-tabs-ink-bar-animated"></div>
</div>
</div>
<div class="ant-tabs-nav-operations ant-tabs-nav-operations-hidden">
<!----><button type="button" class="ant-tabs-nav-more" style="visibility: hidden; order: 1;" tabindex="-1" aria-hidden="true" aria-haspopup="listbox" aria-controls="rc-tabs-test-more-popup" id="rc-tabs-test-more" aria-expanded="false"><span role="img" aria-label="ellipsis" class="anticon anticon-ellipsis"><svg focusable="false" class="" data-icon="ellipsis" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"></path></svg></span></button>
<!---->
</div>
<div class="ant-tabs-extra-content"><a href="#">More</a></div>
<!---->
</div>
<div class="ant-tabs-content-holder">
<div class="ant-tabs-content ant-tabs-content-top">
<div role="tabpanel" tabindex="-1" aria-hidden="true" style="display: none;" class="ant-tabs-tabpane" id="rc-tabs-test-panel-article" aria-labelledby="rc-tabs-test-tab-article">
<!---->
</div>
<div role="tabpanel" tabindex="0" aria-hidden="false" class="ant-tabs-tabpane ant-tabs-tabpane-active" id="rc-tabs-test-panel-app" aria-labelledby="rc-tabs-test-tab-app">
<!---->
</div>
<div role="tabpanel" tabindex="-1" aria-hidden="true" style="display: none;" class="ant-tabs-tabpane" id="rc-tabs-test-panel-project" aria-labelledby="rc-tabs-test-tab-project">
<!---->
</div>
</div>
</div>
<div tabindex="0" style="width: 0px; height: 0px; overflow: hidden; position: absolute;" role="presentation"></div>
<div class="ant-tabs-top-content ant-tabs-content ant-tabs-content-animated" style="margin-left: -100%;">
<div class="ant-tabs-tabpane ant-tabs-tabpane-inactive" role="tabpanel" aria-hidden="true">
<!---->
<!---->
<!---->
</div>
<div class="ant-tabs-tabpane ant-tabs-tabpane-active" role="tabpanel" aria-hidden="false">
<div tabindex="0" style="width: 0px; height: 0px; overflow: hidden; position: absolute;" role="presentation"></div>
<div tabindex="0" style="width: 0px; height: 0px; overflow: hidden; position: absolute;" role="presentation"></div>
</div>
<div class="ant-tabs-tabpane ant-tabs-tabpane-inactive" role="tabpanel" aria-hidden="true">
<!---->
<!---->
<!---->
</div>
</div>
<div tabindex="0" style="width: 0px; height: 0px; overflow: hidden; position: absolute;" role="presentation"></div>
</div>
</div>
<!---->

View File

@ -5,7 +5,7 @@ exports[`Card should still have padding when card which set padding to 0 is load
<!---->
<!---->
<div class="ant-card-body" style="padding: 0px;">
<div class="ant-card-loading-content">
<div class="ant-card-loading-content" style="padding: 24px;">
<div class="ant-row" style="margin-left: -4px; margin-right: -4px;">
<div class="ant-col ant-col-22" style="padding-left: 4px; padding-right: 4px;">
<div class="ant-card-loading-block"></div>

View File

@ -24,8 +24,8 @@ More content can be hosted
:active-tab-key="key"
@tabChange="key => onTabChange(key, 'key')"
>
<template #customRender="item">
<span>
<template #customTab="item">
<span v-if="item.key === 'tab1'">
<home-outlined />
{{ item.key }}
</span>
@ -63,8 +63,7 @@ export default defineComponent({
const tabList = [
{
key: 'tab1',
// tab: 'tab1',
slots: { tab: 'customRender' },
tab: 'tab1',
},
{
key: 'tab2',

View File

@ -17,22 +17,30 @@ A card can be used to display content related to a single subject. The content c
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| actions | The action list, shows at the bottom of the Card. | slots | - | |
| activeTabKey | Current TabPane's key | string | - | |
| headStyle | Inline style to apply to the card head | object | - | |
| bodyStyle | Inline style to apply to the card content | object | - | |
| bordered | Toggles rendering of the border around the card | boolean | `true` | |
| cover | Card cover | slot | - | |
| defaultActiveTabKey | Initial active TabPane's key, if `activeTabKey` is not set. | string | - | |
| extra | Content to render in the top-right corner of the card | string\|slot | - | |
| hoverable | Lift up when hovering card | boolean | false | |
| loading | Shows a loading indicator while the contents of the card are being fetched | boolean | false | |
| tabList | List of TabPane's head, Custom tabs can be created with the slots property | Array&lt;{key: string, tab: any, slots: {tab: 'XXX'}}&gt; | - | |
| tabBarExtraContent | Extra content in tab bar | slot | - | 1.5.0 |
| tabList | List of TabPane's head, Custom tabs with the customTab(v3.0) slot | Array&lt;{key: string, tab: any}&gt; | - | |
| size | Size of card | `default` \| `small` | `default` | |
| title | Card title | string\|slot | - | |
| type | Card style type, can be set to `inner` or not set | string | - | |
### Card Slots
| Slot Name | Description | Type |
| --- | --- | --- | --- |
| customTab | custom tabList tab | { item: tabList[number] } | |
| title | Card title | - | |
| extra | Content to render in the top-right corner of the card | - | |
| tabBarExtraContent | Extra content in tab bar | - | |
| actions | The action list, shows at the bottom of the Card. | - | |
| cover | Card cover | - | |
### events
| Events Name | Description | Arguments | Version |

View File

@ -18,22 +18,30 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/NqXt8DJhky/Card.svg
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| actions | 卡片操作组,位置在卡片底部 | slots | - | |
| activeTabKey | 当前激活页签的 key | string | - | |
| headStyle | 自定义标题区域样式 | object | - | |
| bodyStyle | 内容区域自定义样式 | object | - | |
| bordered | 是否有边框 | boolean | true | |
| cover | 卡片封面 | slot | - | |
| defaultActiveTabKey | 初始化选中页签的 key如果没有设置 activeTabKey | string | 第一个页签 | |
| extra | 卡片右上角的操作区域 | string\|slot | - | |
| hoverable | 鼠标移过时可浮起 | boolean | false | |
| loading | 当卡片内容还在加载中时,可以用 loading 展示一个占位 | boolean | false | |
| tabList | 页签标题列表, 可以通过 slots 属性自定义 tab | Array&lt;{key: string, tab: any, slots: {tab: 'XXX'}}&gt; | - | |
| tabBarExtraContent | tab bar 上额外的元素 | slot | 无 | 1.5.0 |
| tabList | 页签标题列表, 可以通过 customTab(v3.0) 插槽自定义 tab | Array&lt;{key: string, tab: any}&gt; | - | |
| size | card 的尺寸 | `default` \| `small` | `default` | |
| title | 卡片标题 | string\|slot | - | |
| type | 卡片类型,可设置为 `inner` 或 不设置 | string | - | |
### Card 插槽
| 插槽名称 | 说明 | 参数 |
| ------------------ | -------------------------- | ------------------------- | --- |
| customTab | 自定义 tabList tab 标签 | { item: tabList[number] } | |
| title | 卡片标题 | - | |
| extra | 卡片右上角的操作区域 | - | |
| tabBarExtraContent | tab bar 上额外的元素 | - | |
| actions | 卡片操作组,位置在卡片底部 | - | |
| cover | 卡片封面 | - | |
### 事件
| 事件名称 | 说明 | 回调参数 | 版本 |

View File

@ -2,8 +2,7 @@
@import '../../style/mixins/index';
@card-prefix-cls: ~'@{ant-prefix}-card';
@card-head-height: 48px;
@card-hover-border: fade(@black, 9%);
@card-hoverable-hover-border: transparent;
@card-action-icon-size: 16px;
@gradient-min: fade(@card-skeleton-bg, 20%);
@ -15,14 +14,17 @@
position: relative;
background: @card-background;
border-radius: @card-radius;
transition: all 0.3s;
&-rtl {
direction: rtl;
}
&-hoverable {
cursor: pointer;
transition: box-shadow 0.3s border-color 0.3s;
transition: box-shadow 0.3s, border-color 0.3s;
&:hover {
border-color: @card-hover-border;
border-color: @card-hoverable-hover-border;
box-shadow: @card-shadow;
}
}
@ -37,7 +39,7 @@
padding: 0 @card-padding-base;
color: @card-head-color;
font-weight: 500;
font-size: @font-size-lg;
font-size: @card-head-font-size;
background: @card-head-background;
border-bottom: @border-width-base @border-style-base @border-color-split;
border-radius: @card-radius @card-radius 0 0;
@ -55,11 +57,18 @@
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
> .@{ant-prefix}-typography,
> .@{ant-prefix}-typography-edit-content {
left: 0;
margin-top: 0;
margin-bottom: 0;
}
}
.@{ant-prefix}-tabs {
clear: both;
margin-bottom: -17px;
margin-bottom: @card-head-tabs-margin-bottom;
color: @text-color;
font-weight: normal;
font-size: @font-size-base;
@ -75,9 +84,14 @@
// https://stackoverflow.com/a/22429853/3040605
margin-left: auto;
padding: @card-head-padding 0;
color: @text-color;
color: @card-head-extra-color;
font-weight: normal;
font-size: @font-size-base;
.@{card-prefix-cls}-rtl & {
margin-right: auto;
margin-left: 0;
}
}
&-body {
@ -100,11 +114,16 @@
1px 1px 0 0 @border-color-split, 1px 0 0 0 @border-color-split inset,
0 1px 0 0 @border-color-split inset;
transition: all 0.3s;
.@{card-prefix-cls}-rtl & {
float: right;
}
&-hoverable {
&:hover {
position: relative;
z-index: 1;
box-shadow: @box-shadow-base;
box-shadow: @card-shadow;
}
}
}
@ -118,11 +137,18 @@
padding-bottom: 0;
}
&-bordered &-cover {
margin-top: -1px;
margin-right: -1px;
margin-left: -1px;
}
&-cover {
> * {
display: block;
width: 100%;
}
img {
border-radius: @card-radius @card-radius 0 0;
}
@ -138,16 +164,20 @@
& > li {
float: left;
margin: 12px 0;
margin: @card-actions-li-margin;
color: @text-color-secondary;
text-align: center;
.@{card-prefix-cls}-rtl & {
float: right;
}
> span {
position: relative;
display: block;
min-width: 32px;
font-size: @font-size-base;
line-height: 22px;
line-height: @line-height-base;
cursor: pointer;
&:hover {
@ -156,7 +186,7 @@
}
a:not(.@{ant-prefix}-btn),
> .anticon {
> .@{iconfont-css-prefix} {
display: inline-block;
width: 100%;
color: @text-color-secondary;
@ -168,7 +198,7 @@
}
}
> .anticon {
> .@{iconfont-css-prefix} {
font-size: @card-action-icon-size;
line-height: 22px;
}
@ -176,6 +206,11 @@
&:not(:last-child) {
border-right: @border-width-base @border-style-base @border-color-split;
.@{card-prefix-cls}-rtl & {
border-right: none;
border-left: @border-width-base @border-style-base @border-color-split;
}
}
}
}
@ -205,12 +240,18 @@
&-avatar {
float: left;
padding-right: 16px;
.@{card-prefix-cls}-rtl & {
float: right;
padding-right: 0;
padding-left: 16px;
}
}
&-detail {
overflow: hidden;
> div:not(:last-child) {
margin-bottom: 8px;
margin-bottom: @margin-xs;
}
}

View File

@ -1,8 +1,3 @@
@card-head-height-sm: 36px;
@card-padding-base-sm: (@card-padding-base / 2);
@card-head-padding-sm: (@card-head-padding / 2);
@card-head-font-size-sm: @font-size-base;
.@{card-prefix-cls}-small {
> .@{card-prefix-cls}-head {
min-height: @card-head-height-sm;

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

@ -27,7 +27,7 @@ exports[`renders ./components/menu/demo/horizontal.vue correctly 1`] = `
`;
exports[`renders ./components/menu/demo/inline.vue correctly 1`] = `
<ul class="ant-menu ant-menu-root ant-menu-inline ant-menu-light" style="width: 256px;" role="menu" data-menu-list="true" id="dddddd">
<ul id="dddddd" class="ant-menu ant-menu-root ant-menu-inline ant-menu-light" style="width: 256px;" role="menu" data-menu-list="true">
<li class="ant-menu-submenu ant-menu-submenu-inline ant-menu-submenu-open ant-menu-submenu-selected" role="none" data-submenu-id="sub1">
<!--teleport start-->
<!--teleport end-->

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

@ -1135,10 +1135,10 @@ exports[`renders ./components/page-header/demo/ghost.vue correctly 1`] = `
exports[`renders ./components/page-header/demo/responsive.vue correctly 1`] = `
<div
class="components-page-header-demo-responsive"
style="border: 1px solid rgb(235, 237, 240);"
>
<div
class="ant-page-header has-footer ant-page-header-ghost"
style="border: 1px solid rgb(235, 237, 240);"
>
<!---->
<div
@ -1464,174 +1464,132 @@ exports[`renders ./components/page-header/demo/responsive.vue correctly 1`] = `
>
<div
class="ant-tabs-line ant-tabs ant-tabs-top"
class="ant-tabs ant-tabs-top"
>
<div
class="ant-tabs-bar ant-tabs-top-bar"
class="ant-tabs-nav"
role="tablist"
tabindex="0"
>
<!---->
<div
class="ant-tabs-nav-container"
class="ant-tabs-nav-wrap"
>
<span
class="ant-tabs-tab-prev ant-tabs-tab-btn-disabled"
unselectable="unselectable"
>
<span
class="ant-tabs-tab-prev-icon"
>
<span
aria-label="left"
class="anticon anticon-left ant-tabs-tab-prev-icon-target"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="left"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"
/>
</svg>
</span>
</span>
</span>
<span
class="ant-tabs-tab-next ant-tabs-tab-btn-disabled"
unselectable="unselectable"
>
<span
class="ant-tabs-tab-next-icon"
>
<span
aria-label="right"
class="anticon anticon-right ant-tabs-tab-next-icon-target"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="right"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
/>
</svg>
</span>
</span>
</span>
<div
class="ant-tabs-nav-wrap"
class="ant-tabs-nav-list"
style="transform: translate(0px, 0px);"
>
<div
class="ant-tabs-nav-scroll"
class="ant-tabs-tab ant-tabs-tab-active"
>
<div
class="ant-tabs-nav ant-tabs-nav-animated"
aria-controls="rc-tabs-test-panel-1"
aria-selected="true"
class="ant-tabs-tab-btn"
id="rc-tabs-test-tab-1"
role="tab"
tabindex="0"
>
<div>
<div
aria-disabled="false"
aria-selected="true"
class="ant-tabs-tab-active ant-tabs-tab"
role="tab"
>
Details
</div>
<div
aria-disabled="false"
aria-selected="false"
class="ant-tabs-tab"
role="tab"
>
Rule
</div>
</div>
<div
class="ant-tabs-ink-bar ant-tabs-ink-bar-animated"
style="display: block; transform: translate3d(0px,0,0); webkit-transform: translate3d(0px,0,0); width: 0px;"
/>
Details
</div>
<!---->
</div>
<div
class="ant-tabs-tab"
>
<div
aria-controls="rc-tabs-test-panel-2"
aria-selected="false"
class="ant-tabs-tab-btn"
id="rc-tabs-test-tab-2"
role="tab"
tabindex="0"
>
Rule
</div>
<!---->
</div>
</div>
</div>
</div>
<div
role="presentation"
style="width: 0px; height: 0px; overflow: hidden; position: absolute;"
tabindex="0"
>
</div>
<div
class="ant-tabs-top-content ant-tabs-content ant-tabs-content-animated"
style="margin-left: 0%;"
>
<div
aria-hidden="false"
class="ant-tabs-tabpane ant-tabs-tabpane-active"
role="tabpanel"
>
<div
role="presentation"
style="width: 0px; height: 0px; overflow: hidden; position: absolute;"
tabindex="0"
>
</div>
<div
role="presentation"
style="width: 0px; height: 0px; overflow: hidden; position: absolute;"
tabindex="0"
>
<!---->
<div
class="ant-tabs-ink-bar ant-tabs-ink-bar-animated"
/>
</div>
</div>
<div
aria-hidden="true"
class="ant-tabs-tabpane ant-tabs-tabpane-inactive"
role="tabpanel"
class="ant-tabs-nav-operations ant-tabs-nav-operations-hidden"
>
<!---->
<!---->
<button
aria-controls="rc-tabs-test-more-popup"
aria-expanded="false"
aria-haspopup="listbox"
aria-hidden="true"
class="ant-tabs-nav-more"
id="rc-tabs-test-more"
style="visibility: hidden; order: 1;"
tabindex="-1"
type="button"
>
<span
aria-label="ellipsis"
class="anticon anticon-ellipsis"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="ellipsis"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"
/>
</svg>
</span>
</button>
<!---->
</div>
<!---->
<!---->
</div>
<div
role="presentation"
style="width: 0px; height: 0px; overflow: hidden; position: absolute;"
tabindex="0"
class="ant-tabs-content-holder"
>
<div
class="ant-tabs-content ant-tabs-content-top"
>
<div
aria-hidden="false"
aria-labelledby="rc-tabs-test-tab-1"
class="ant-tabs-tabpane ant-tabs-tabpane-active"
id="rc-tabs-test-panel-1"
role="tabpanel"
tabindex="0"
>
<!---->
</div>
<div
aria-hidden="true"
aria-labelledby="rc-tabs-test-tab-2"
class="ant-tabs-tabpane"
id="rc-tabs-test-panel-2"
role="tabpanel"
style="display: none;"
tabindex="-1"
>
<!---->
</div>
</div>
</div>
</div>
</div>

View File

@ -18,20 +18,15 @@ Under different screen sizes, there should be different performance
</docs>
<template>
<div class="components-page-header-demo-responsive">
<a-page-header
style="border: 1px solid rgb(235, 237, 240)"
title="Title"
sub-title="This is a subtitle"
@back="() => $router.go(-1)"
>
<div class="components-page-header-demo-responsive" style="border: 1px solid rgb(235, 237, 240)">
<a-page-header title="Title" sub-title="This is a subtitle" @back="() => $router.go(-1)">
<template #extra>
<a-button key="3">Operation</a-button>
<a-button key="2">Operation</a-button>
<a-button key="1" type="primary">Primary</a-button>
</template>
<template #footer>
<a-tabs default-active-key="1">
<a-tabs>
<a-tab-pane key="1" tab="Details" />
<a-tab-pane key="2" tab="Rule" />
</a-tabs>
@ -73,6 +68,9 @@ Under different screen sizes, there should be different performance
</div>
</template>
<style>
.components-page-header-demo-responsive {
padding-bottom: 20px;
}
.components-page-header-demo-responsive tr:last-child td {
padding-bottom: 0;
}

View File

@ -11,7 +11,7 @@ import Tooltip from '../tooltip';
import useConfigInject from '../_util/hooks/useConfigInject';
import Star from './Star';
import { useRef } from '../_util/hooks/useRef';
import useRefs from '../_util/hooks/useRefs';
import { useInjectFormItemContext } from '../form/FormItemContext';
export const rateProps = {
@ -48,7 +48,7 @@ const Rate = defineComponent({
const { prefixCls, direction } = useConfigInject('rate', props);
const formItemContext = useInjectFormItemContext();
const rateRef = ref();
const [setRef, starRefs] = useRef();
const [setRef, starRefs] = useRefs();
const state = reactive({
value: props.value,
focused: false,
@ -62,7 +62,7 @@ const Rate = defineComponent({
},
);
const getStarDOM = (index: number) => {
return findDOMNode(starRefs.value[index]);
return findDOMNode(starRefs.value.get(index));
};
const getStarValue = (index: number, x: number) => {
const reverse = direction.value === 'rtl';
@ -199,7 +199,7 @@ const Rate = defineComponent({
for (let index = 0; index < count; index++) {
stars.push(
<Star
ref={(r: any) => setRef(r, index)}
ref={setRef(index)}
key={index}
index={index}
count={count}

View File

@ -657,14 +657,24 @@
// ---
@card-head-color: @heading-color;
@card-head-background: transparent;
@card-head-font-size: @font-size-lg;
@card-head-font-size-sm: @font-size-base;
@card-head-padding: 16px;
@card-head-padding-sm: (@card-head-padding / 2);
@card-head-height: 48px;
@card-head-height-sm: 36px;
@card-inner-head-padding: 12px;
@card-padding-base: 24px;
@card-actions-background: @background-color-light;
@card-padding-base-sm: (@card-padding-base / 2);
@card-actions-background: @component-background;
@card-actions-li-margin: 12px 0;
@card-skeleton-bg: #cfd8dc;
@card-background: @component-background;
@card-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
@card-radius: @border-radius-sm;
@card-shadow: 0 1px 2px -2px rgba(0, 0, 0, 0.16), 0 3px 6px 0 rgba(0, 0, 0, 0.12),
0 5px 12px 4px rgba(0, 0, 0, 0.09);
@card-radius: @border-radius-base;
@card-head-tabs-margin-bottom: -17px;
@card-head-extra-color: @text-color;
// Comment
// ---
@ -686,17 +696,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;

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Tabs tabPosition remove card 1`] = `
<div class="ant-tabs-vertical ant-tabs-line ant-tabs ant-tabs-left">
<div role="tablist" class="ant-tabs-bar ant-tabs-left-bar" tabindex="0">
<div class="ant-tabs-nav-container"><span unselectable="unselectable" class="ant-tabs-tab-prev ant-tabs-tab-btn-disabled"><span class="ant-tabs-tab-prev-icon"><span role="img" aria-label="up" class="anticon anticon-up ant-tabs-tab-prev-icon-target"><svg focusable="false" class="" data-icon="up" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"></path></svg></span></span></span><span unselectable="unselectable" class="ant-tabs-tab-next ant-tabs-tab-btn-disabled"><span class="ant-tabs-tab-next-icon"><span role="img" aria-label="down" class="anticon anticon-down ant-tabs-tab-next-icon-target"><svg focusable="false" class="" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path></svg></span></span></span>
<div class="ant-tabs-nav-wrap">
<div class="ant-tabs-nav-scroll">
<div class="ant-tabs-nav ant-tabs-nav-animated">
<div>
<div role="tab" aria-disabled="false" aria-selected="true" class="ant-tabs-tab-active ant-tabs-tab">foo</div>
</div>
<div class="ant-tabs-ink-bar ant-tabs-ink-bar-animated"></div>
</div>
<div class="ant-tabs ant-tabs-left">
<div role="tablist" class="ant-tabs-nav">
<!---->
<div class="ant-tabs-nav-wrap">
<div class="ant-tabs-nav-list" style="transform: translate(0px, 0px);">
<div class="ant-tabs-tab ant-tabs-tab-active">
<div role="tab" aria-selected="true" class="ant-tabs-tab-btn" tabindex="0">foo</div>
<!---->
</div>
<!---->
<div class="ant-tabs-ink-bar ant-tabs-ink-bar-animated"></div>
</div>
</div>
<div class="ant-tabs-nav-operations ant-tabs-nav-operations-hidden">
<!----><button type="button" class="ant-tabs-nav-more" style="visibility: hidden; order: 1;" tabindex="-1" aria-hidden="true" aria-haspopup="listbox" aria-controls="null-more-popup" id="null-more" aria-expanded="false"><span role="img" aria-label="ellipsis" class="anticon anticon-ellipsis"><svg focusable="false" class="" data-icon="ellipsis" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"></path></svg></span></button>
<!---->
</div>
<div class="ant-tabs-extra-content">xxx</div>
<!---->
</div>
<div tabindex="0" style="width: 0px; height: 0px; overflow: hidden; position: absolute;" role="presentation"></div>
<div class="ant-tabs-left-content ant-tabs-content ant-tabs-content-animated" style="margin-top: 0%;">
<div class="ant-tabs-tabpane ant-tabs-tabpane-active" role="tabpanel" aria-hidden="false">
<div tabindex="0" style="width: 0px; height: 0px; overflow: hidden; position: absolute;" role="presentation"></div>foo<div tabindex="0" style="width: 0px; height: 0px; overflow: hidden; position: absolute;" role="presentation"></div>
<div class="ant-tabs-content-holder">
<div class="ant-tabs-content ant-tabs-content-left">
<div role="tabpanel" tabindex="0" aria-hidden="false" class="ant-tabs-tabpane ant-tabs-tabpane-active">foo</div>
</div>
</div>
<div tabindex="0" style="width: 0px; height: 0px; overflow: hidden; position: absolute;" role="presentation"></div>
</div>
`;

View File

@ -22,7 +22,7 @@ describe('Tabs', () => {
});
it('add card', () => {
wrapper.find('.ant-tabs-new-tab').trigger('click');
wrapper.find('.ant-tabs-nav-add').trigger('click');
expect(handleEdit.mock.calls[0][1]).toBe('add');
});
@ -37,7 +37,7 @@ describe('Tabs', () => {
const wrapper = mount({
render() {
return (
<Tabs tabPosition="left" tabBarExtraContent="xxx">
<Tabs tabPosition="left" v-slots={{ rightExtra: () => 'xxx' }}>
<TabPane tab="foo" key="1">
foo
</TabPane>

View File

@ -42,38 +42,57 @@ import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
return {
activeKey: ref(1),
activeKey: ref('2'),
};
},
});
</script>
<style>
.card-container {
background: #f5f5f5;
overflow: hidden;
padding: 24px;
.card-container p {
margin: 0;
}
.card-container > .ant-tabs-card > .ant-tabs-content {
.card-container > .ant-tabs-card .ant-tabs-content {
height: 120px;
margin-top: -16px;
}
.card-container > .ant-tabs-card > .ant-tabs-content > .ant-tabs-tabpane {
background: #fff;
.card-container > .ant-tabs-card .ant-tabs-content > .ant-tabs-tabpane {
padding: 16px;
}
.card-container > .ant-tabs-card > .ant-tabs-bar {
border-color: #fff;
}
.card-container > .ant-tabs-card > .ant-tabs-bar .ant-tabs-tab {
border-color: transparent;
background: transparent;
}
.card-container > .ant-tabs-card > .ant-tabs-bar .ant-tabs-tab-active {
border-color: #fff;
background: #fff;
}
.card-container > .ant-tabs-card > .ant-tabs-nav::before {
display: none;
}
.card-container > .ant-tabs-card .ant-tabs-tab,
[data-theme='compact'] .card-container > .ant-tabs-card .ant-tabs-tab {
background: transparent;
border-color: transparent;
}
.card-container > .ant-tabs-card .ant-tabs-tab-active,
[data-theme='compact'] .card-container > .ant-tabs-card .ant-tabs-tab-active {
background: #fff;
border-color: #fff;
}
#components-tabs-demo-card-top .code-box-demo {
padding: 24px;
overflow: hidden;
background: #f5f5f5;
}
[data-theme='compact'] .card-container > .ant-tabs-card .ant-tabs-content {
height: 120px;
margin-top: -8px;
}
[data-theme='dark'] .card-container > .ant-tabs-card .ant-tabs-tab {
background: transparent;
border-color: transparent;
}
[data-theme='dark'] #components-tabs-demo-card-top .code-box-demo {
background: #000;
}
[data-theme='dark'] .card-container > .ant-tabs-card .ant-tabs-content > .ant-tabs-tabpane {
background: #141414;
}
[data-theme='dark'] .card-container > .ant-tabs-card .ant-tabs-tab-active {
background: #141414;
border-color: #141414;
}
</style>

View File

@ -0,0 +1,36 @@
<docs>
---
order: 2
title:
zh-CN: 居中
en-US: Centered
---
## zh-CN
标签居中展示
## en-US
Centered tabs.
</docs>
<template>
<a-tabs v-model:activeKey="activeKey" centered>
<a-tab-pane key="1" tab="Tab 1">Content of Tab Pane 1</a-tab-pane>
<a-tab-pane key="2" tab="Tab 2" force-render>Content of Tab Pane 2</a-tab-pane>
<a-tab-pane key="3" tab="Tab 3">Content of Tab Pane 3</a-tab-pane>
</a-tabs>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
return {
activeKey: ref('1'),
};
},
});
</script>

View File

@ -33,7 +33,7 @@ import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
const panes = ref([
const panes = ref<{ title: string; content: string; key: string; closable?: boolean }[]>([
{ title: 'Tab 1', content: 'Content of Tab 1', key: '1' },
{ title: 'Tab 2', content: 'Content of Tab 2', key: '2' },
]);

View File

@ -22,11 +22,7 @@ Customized bar of tab.
<a-tab-pane key="2" tab="Tab 2" force-render>Content of Tab Pane 2</a-tab-pane>
<a-tab-pane key="3" tab="Tab 3">Content of Tab Pane 3</a-tab-pane>
<template #renderTabBar="{ DefaultTabBar, ...props }">
<component
:is="DefaultTabBar"
v-bind="props"
:style="{ zIndex: 1, background: '#fff', textAlign: 'right' }"
/>
<component :is="DefaultTabBar" v-bind="props" :style="{ opacity: 0.5 }" />
</template>
</a-tabs>
</div>

View File

@ -21,8 +21,11 @@ You can add extra actions to the right of Tabs.
<a-tab-pane key="1" tab="Tab 1">Content of tab 1</a-tab-pane>
<a-tab-pane key="2" tab="Tab 2">Content of tab 2</a-tab-pane>
<a-tab-pane key="3" tab="Tab 3">Content of tab 3</a-tab-pane>
<template #tabBarExtraContent>
<a-button>Extra Action</a-button>
<template #leftExtra>
<a-button class="tabs-extra-demo-button">Left Extra Action</a-button>
</template>
<template #rightExtra>
<a-button>Right Extra Action</a-button>
</template>
</a-tabs>
</template>
@ -37,3 +40,13 @@ export default defineComponent({
},
});
</script>
<style>
.tabs-extra-demo-button {
margin-right: 16px;
}
.ant-row-rtl .tabs-extra-demo-button {
margin-right: 0;
margin-left: 16px;
}
</style>

View File

@ -2,6 +2,7 @@
<demo-sort :cols="1">
<Basic />
<Disabled />
<Centered />
<Icon />
<Slide />
<Extra />
@ -28,6 +29,7 @@ import Position from './position.vue';
import Size from './size.vue';
import Slide from './slide.vue';
import CustomTabBar from './custom-tab-bar.vue';
import Centered from './centered.vue';
import CN from '../index.zh-CN.md';
import US from '../index.en-US.md';
import { defineComponent } from 'vue';
@ -40,6 +42,7 @@ export default defineComponent({
CN,
US,
components: {
Centered,
Basic,
Card,
CardTop,

View File

@ -8,28 +8,26 @@ title:
## zh-CN
有四个位置`tabPosition="left|right|top|bottom"`
有四个位置`tabPosition="left|right|top|bottom"`在移动端下`bottom|right` 会自动切换成 `top`
## en-US
Tab's position: left, right, top or bottom.
Tab's position: left, right, top or bottom. Will auto switch to `top` in mobile.
</docs>
<template>
<div style="width: 500px">
<a-radio-group v-model:value="tabPosition" style="margin: 8px">
<a-radio-button value="top">top</a-radio-button>
<a-radio-button value="bottom">bottom</a-radio-button>
<a-radio-button value="left">left</a-radio-button>
<a-radio-button value="right">right</a-radio-button>
</a-radio-group>
<a-tabs v-model:activeKey="activeKey" :tab-position="tabPosition">
<a-tab-pane key="1" tab="Tab 1">Content of Tab 1</a-tab-pane>
<a-tab-pane key="2" tab="Tab 2">Content of Tab 2</a-tab-pane>
<a-tab-pane key="3" tab="Tab 3">Content of Tab 3</a-tab-pane>
</a-tabs>
</div>
<a-radio-group v-model:value="tabPosition" style="margin: 8px">
<a-radio-button value="top">top</a-radio-button>
<a-radio-button value="bottom">bottom</a-radio-button>
<a-radio-button value="left">left</a-radio-button>
<a-radio-button value="right">right</a-radio-button>
</a-radio-group>
<a-tabs v-model:activeKey="activeKey" :tab-position="tabPosition">
<a-tab-pane key="1" tab="Tab 1">Content of Tab 1</a-tab-pane>
<a-tab-pane key="2" tab="Tab 2">Content of Tab 2</a-tab-pane>
<a-tab-pane key="3" tab="Tab 3">Content of Tab 3</a-tab-pane>
</a-tabs>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';

View File

@ -17,7 +17,7 @@ In order to fit in more tabs, they can slide left and right (or up and down).
</docs>
<template>
<div style="width: 500px">
<div>
<a-radio-group v-model:value="mode" :style="{ marginBottom: '8px' }">
<a-radio-button value="top">Horizontal</a-radio-button>
<a-radio-button value="left">Vertical</a-radio-button>
@ -26,8 +26,7 @@ In order to fit in more tabs, they can slide left and right (or up and down).
v-model:activeKey="activeKey"
:tab-position="mode"
:style="{ height: '200px' }"
@prevClick="callback"
@nextClick="callback"
@tabScroll="callback"
>
<a-tab-pane v-for="i in 30" :key="i" :tab="`Tab-${i}`">Content of tab {{ i }}</a-tab-pane>
</a-tabs>

View File

@ -19,20 +19,28 @@ Ant Design has 3 types of Tabs for different situations.
### Tabs
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| activeKey(v-model) | Current TabPane's key | string | - |
| animated | Whether to change tabs with animation. Only works while `tabPosition="top"\|"bottom"` | boolean \| {inkBar:boolean, tabPane:boolean} | `true`, `false` when `type="card"` |
| defaultActiveKey | Initial active TabPane's key, if `activeKey` is not set. | string | - |
| hideAdd | Hide plus icon or not. Only works while `type="editable-card"` | boolean | `false` |
| size | preset tab bar size | `large` \| `default` \| `small` | `default` |
| tabBarExtraContent | Extra content in tab bar | slot | - |
| tabBarStyle | Tab bar style object | object | - |
| tabPosition | Position of tabs | `top` \| `right` \| `bottom` \| `left` | `top` |
| type | Basic style of tabs | `line` \| `card` \| `editable-card` | `line` |
| tabBarGutter | The gap between tabs | number | - |
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| activeKey(v-model) | Current TabPane's key | string | - | |
| animated | Whether to change tabs with animation. Only works while `tabPosition="top"\|"bottom"` | boolean \| {inkBar:boolean, tabPane:boolean} | `true`, `false` when `type="card"` | |
| hideAdd | Hide plus icon or not. Only works while `type="editable-card"` | boolean | `false` | } |
| size | preset tab bar size | `large` \| `default` \| `small` | `default` | |
| tabBarStyle | Tab bar style object | object | - | |
| tabPosition | Position of tabs | `top` \| `right` \| `bottom` \| `left` | `top` | |
| type | Basic style of tabs | `line` \| `card` \| `editable-card` | `line` | |
| tabBarGutter | The gap between tabs | number | - | |
### Events
### Tabs Slots
| Slot Name | Description | Type |
| ------------ | ------------------------------ | ----------------- | --- |
| renderTabBar | Replace the TabBar | { DefaultTabBar } | |
| leftExtra | Extra content in tab bar left | - | - |
| rightExtra | Extra content in tab bar right | - | - |
| addIcon | Customize add icon | - | - |
| moreIcon | The custom icon of ellipsis | - | - |
### Tabs Events
| Events Name | Description | Arguments |
| --- | --- | --- |
@ -49,3 +57,10 @@ Ant Design has 3 types of Tabs for different situations.
| forceRender | Forced render of content in tabs, not lazy render after clicking on tabs | boolean | false |
| key | TabPane's key | string | - |
| tab | Show text in TabPane's head | string\|slot | - |
### Tabs.TabPane Slots
| 插槽名称 | 说明 | 参数 |
| --------- | ----------------------------------------------- | ---- |
| closeIcon | 自定义关闭图标,`在 type="editable-card"`时有效 | - |
| tab | Show text in TabPane's head | - |

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

@ -22,28 +22,36 @@ Ant Design 依次提供了三级选项卡,分别用于不同的场景。
### Tabs
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- | --- |
| activeKey(v-model) | 当前激活 tab 面板的 key | string | 无 |
| animated | 是否使用动画切换 Tabs`tabPosition=top | bottom` 时有效 | boolean \| {inkBar:boolean, tabPane:boolean} | true, 当 type="card" 时为 false |
| defaultActiveKey | 初始化选中面板的 key如果没有设置 activeKey | string | 第一个面板 |
| hideAdd | 是否隐藏加号图标,在 `type="editable-card"` 时有效 | boolean | false |
| size | 大小,提供 `large` `default``small` 三种大小 | string | 'default' |
| tabBarExtraContent | tab bar 上额外的元素 | slot | 无 |
| tabBarStyle | tab bar 的样式对象 | object | - |
| tabPosition | 页签位置,可选值有 `top` `right` `bottom` `left` | string | 'top' |
| type | 页签的基本样式,可选 `line`、`card` `editable-card` 类型 | string | 'line' |
| tabBarGutter | tabs 之间的间隙 | number | 无 |
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- | --- |
| activeKey(v-model) | 当前激活 tab 面板的 key | string | 无 | |
| animated | 是否使用动画切换 Tabs`tabPosition=top | bottom` 时有效 | boolean \| {inkBar:boolean, tabPane:boolean} | true, 当 type="card" 时为 false | |
| centered | 标签居中展示 | boolean | false | 3.0 |
| hideAdd | 是否隐藏加号图标,在 `type="editable-card"` 时有效 | boolean | false | |
| size | 大小,提供 `large` `default``small` 三种大小 | string | 'default' | |
| tabBarStyle | tab bar 的样式对象 | object | - | |
| tabPosition | 页签位置,可选值有 `top` `right` `bottom` `left` | string | 'top' | |
| type | 页签的基本样式,可选 `line`、`card` `editable-card` 类型 | string | 'line' | |
| tabBarGutter | tabs 之间的间隙 | number | 无 | |
### 事件
### Tabs 插槽
| 事件名称 | 说明 | 回调参数 |
| --------- | ------------------------------------------------------ | ------------------------- |
| change | 切换面板的回调 | Function(activeKey) {} |
| edit | 新增和删除页签的回调,在 `type="editable-card"` 时有效 | (targetKey, action): void |
| nextClick | next 按钮被点击的回调 | Function |
| prevClick | prev 按钮被点击的回调 | Function |
| tabClick | tab 被点击的回调 | Function |
| 插槽名称 | 说明 | 参数 |
| ------------ | ------------------------------- | ----------------- | --- |
| renderTabBar | 替换 TabBar用于二次封装标签头 | { DefaultTabBar } | |
| leftExtra | tab bar 上左侧额外的元素 | - | - |
| rightExtra | tab bar 上右侧额外的元素 | - | - |
| addIcon | 自定义添加按钮 | - | - |
| moreIcon | 自定义折叠 icon | - | - |
### Tabs 事件
| 事件名称 | 说明 | 回调参数 |
| --- | --- | --- |
| change | 切换面板的回调 | Function(activeKey) {} |
| edit | 新增和删除页签的回调,在 `type="editable-card"` 时有效 | (targetKey, action): void |
| tabScroll | 滚动 TabBar 是触发 | { direction: 'left' \| 'right' \| 'top' \| 'bottom' } |
| tabClick | tab 被点击的回调 | Function |
### Tabs.TabPane
@ -52,3 +60,10 @@ Ant Design 依次提供了三级选项卡,分别用于不同的场景。
| forceRender | 被隐藏时是否渲染 DOM 结构 | boolean | false |
| key | 对应 activeKey | string | 无 |
| tab | 选项卡头显示文字 | string\|slot | 无 |
### Tabs.TabPane 插槽
| 插槽名称 | 说明 | 参数 |
| --------- | ----------------------------------------------- | ---- |
| closeIcon | 自定义关闭图标,`在 type="editable-card"`时有效 | - |
| tab | 选项卡头显示文字 | - |

View File

@ -0,0 +1,33 @@
import type { Tab } from './interface';
import type { PropType, InjectionKey, Ref } from 'vue';
import { provide, inject, defineComponent, toRefs, ref } from 'vue';
export interface TabContextProps {
tabs: Ref<Tab[]>;
prefixCls: Ref<string>;
}
const TabsContextKey: InjectionKey<TabContextProps> = Symbol('tabsContextKey');
export const useProvideTabs = (props: TabContextProps) => {
provide(TabsContextKey, props);
};
export const useInjectTabs = () => {
return inject(TabsContextKey, { tabs: ref([]), prefixCls: ref() });
};
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(toRefs(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,218 @@
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';
import { EllipsisOutlined } from '@ant-design/icons-vue';
export interface OperationNodeProps {
prefixCls: string;
id: string;
tabs: Tab[];
rtl: boolean;
tabBarGutter?: number;
activeKey: Key;
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, Number] },
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<Key>(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?.() || <EllipsisOutlined />,
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 { 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}
>
{typeof tab === 'function' ? tab() : 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,542 @@
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 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';
import useRefs from '../../../_util/hooks/useRefs';
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, Number] },
rtl: { type: Boolean },
animated: { type: Object as PropType<AnimatedConfig>, default: undefined as AnimatedConfig },
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?: (info?: { position: 'left' | 'right' }) => TabBarExtraContent;
}
export default defineComponent({
name: 'TabNavList',
inheritAttrs: false,
props: tabNavListProps(),
slots: ['moreIcon', 'leftExtra', 'rightExtra', 'tabBarExtraContent'],
emits: ['tabClick', 'tabScroll'],
setup(props, { attrs, slots }) {
const { tabs, prefixCls } = useInjectTabs();
const tabsWrapperRef = ref<HTMLDivElement>();
const tabListRef = ref<HTMLDivElement>();
const operationsRef = ref<{ $el: HTMLDivElement }>();
const innerAddButtonRef = ref<HTMLButtonElement>();
const [setRef, btnRefs] = 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(tabs, tabSizes);
// ========================== Util =========================
const operationsHiddenClassName = computed(() => `${prefixCls.value}-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 = wrapperWidth.value;
tabContentSize = wrapperContentWidth.value;
addSize = addWidth.value;
position = props.rtl ? 'right' : 'left';
transformSize = Math.abs(transformLeft.value);
} else {
unit = 'height';
basicSize = wrapperHeight.value;
tabContentSize = wrapperContentHeight.value;
addSize = addHeight.value;
position = 'top';
transformSize = -transformTop.value;
}
let mergedBasicSize = basicSize;
if (tabContentSize + addSize > basicSize) {
mergedBasicSize = basicSize - addSize;
}
const tabsVal = tabs.value;
if (!tabsVal.length) {
return ([visibleStart.value, visibleEnd.value] = [0, 0]);
}
const len = tabsVal.length;
let endIndex = len;
for (let i = 0; i < len; i += 1) {
const offset = tabOffsets.value.get(tabsVal[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(tabsVal[i].key) || DEFAULT_SIZE;
if (offset[position] < transformSize) {
startIndex = i + 1;
break;
}
}
return ([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();
tabs.value.forEach(({ key }) => {
const btnRef = btnRefs.value.get(key);
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(() => [
...tabs.value.slice(0, visibleStart.value),
...tabs.value.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, () => tabs.value],
() => {
onListHolderResize();
},
{ flush: 'post' },
);
const ExtraContent = ({ position, prefixCls, extra }: ExtraContentProps) => {
if (!extra) return null;
const content = extra?.({ position });
return content ? <div class={`${prefixCls}-extra-content`}>{content}</div> : null;
};
onBeforeUnmount(() => {
clearTouchMoving();
cleanInkBarRaf();
});
return () => {
const {
id,
animated,
activeKey,
rtl,
editable,
locale,
tabPosition,
tabBarGutter,
onTabClick,
} = props;
const { class: className, style } = attrs;
const pre = prefixCls.value;
// ========================= Render ========================
const hasDropdown = !!hiddenTabs.value.length;
const wrapPrefix = `${pre}-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.value.map((tab, i) => {
const { key } = tab;
return (
<TabNode
id={id}
prefixCls={pre}
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={setRef(key)}
onClick={e => {
onTabClick(key, e);
}}
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(`${pre}-nav`, className)}
style={style}
onKeydown={() => {
// No need animation when use keyboard
doLockAnimation();
}}
>
<ExtraContent position="left" prefixCls={pre} extra={slots.leftExtra} />
<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={`${pre}-nav-list`}
style={{
transform: `translate(${transformLeft.value}px, ${transformTop.value}px)`,
transition: lockAnimation.value ? 'none' : undefined,
}}
>
{tabNodes}
<AddButton
ref={innerAddButtonRef}
prefixCls={pre}
locale={locale}
editable={editable}
style={{
...(tabNodes.length === 0 ? undefined : tabNodeStyle),
visibility: hasDropdown ? 'hidden' : null,
}}
/>
<div
class={classNames(`${pre}-ink-bar`, {
[`${pre}-ink-bar-animated`]: animated.inkBar,
})}
style={inkStyle.value}
/>
</div>
</ResizeObserver>
</div>
</ResizeObserver>
<OperationNode
{...props}
ref={operationsRef}
prefixCls={pre}
tabs={hiddenTabs.value}
class={!hasDropdown && operationsHiddenClassName.value}
/>
<ExtraContent position="right" prefixCls={pre} extra={slots.rightExtra} />
<ExtraContent position="right" prefixCls={pre} extra={slots.tabBarExtraContent} />
</div>
);
};
},
});

View File

@ -0,0 +1,85 @@
import { defineComponent, ref, watch, computed } from 'vue';
import type { CSSProperties } from 'vue';
import type { VueNode, Key } 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?: Key;
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, Number] },
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,66 @@
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 { tabs, prefixCls } = useInjectTabs();
return () => {
const { id, activeKey, animated, tabPosition, rtl, destroyInactiveTabPane } = props;
const tabPaneAnimated = animated.tabPane;
const pre = prefixCls.value;
const activeIndex = tabs.value.findIndex(tab => tab.key === activeKey);
return (
<div class={`${pre}-content-holder`}>
<div
class={[
`${pre}-content`,
`${pre}-content-${tabPosition}`,
{
[`${pre}-content-animated`]: tabPaneAnimated,
},
]}
style={
activeIndex && tabPaneAnimated
? { [rtl ? 'marginRight' : 'marginLeft']: `-${activeIndex}00%` }
: null
}
>
{tabs.value.map(tab => {
return cloneElement(tab.node, {
key: tab.key,
prefixCls: pre,
tabKey: tab.key,
id,
animated: tabPaneAnimated,
active: tab.key === activeKey,
destroyInactiveTabPane,
});
})}
</div>
</div>
);
};
},
});

View File

@ -0,0 +1,357 @@
// 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 { useProvideTabs } from './TabContext';
import type { Key } from '../../_util/type';
import pick from 'lodash-es/pick';
import PropTypes from '../../_util/vue-types';
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, Number] },
defaultActiveKey: { type: [String, Number] },
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 | Key, action: 'add' | 'remove') => void
>,
},
onChange: { type: Function as PropType<(activeKey: Key) => void> },
onTabClick: {
type: Function as PropType<(activeKey: Key, 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,
tabBarExtraContent: PropTypes.any,
};
};
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 ? 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',
'leftExtra',
'rightExtra',
'moreIcon',
'addIcon',
'removeIcon',
'renderTabBar',
],
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.',
);
devWarning(
!(props.tabBarExtraContent !== undefined),
'Tabs',
'`tabBarExtraContent` prop has been removed. Please use `rightExtra` slot instead.',
);
devWarning(
!(slots.tabBarExtraContent !== undefined),
'Tabs',
'`tabBarExtraContent` slot is deprecated. Please use `rightExtra` slot 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<Key>(() => 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));
mergedActiveKey.value = 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: Key, e: MouseEvent | KeyboardEvent) => {
props.onTabClick?.(key, e);
setMergedActiveKey(key);
props.onChange?.(key);
};
useProvideTabs({
tabs: computed(() => props.tabs),
prefixCls,
});
return () => {
const {
id,
type,
tabBarGutter,
tabBarStyle,
locale,
destroyInactiveTabPane,
renderTabBar = slots.renderTabBar,
onTabScroll,
hideAdd,
centered,
} = 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,
};
if (renderTabBar) {
tabNavBar = renderTabBar({ ...tabNavBarProps, DefaultTabBar: TabNavList });
} else {
tabNavBar = (
<TabNavList
{...tabNavBarProps}
v-slots={pick(slots, ['moreIcon', 'leftExtra', 'rightExtra', 'tabBarExtraContent'])}
/>
);
}
const pre = prefixCls.value;
return (
<div
{...attrs}
id={id}
class={classNames(
pre,
`${pre}-${mergedTabPosition.value}`,
{
[`${pre}-${size.value}`]: 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,
)}
>
{tabNavBar}
<TabPanelList
destroyInactiveTabPane={destroyInactiveTabPane}
{...sharedProps}
animated={mergedAnimated.value}
/>
</div>
);
};
},
});
export default defineComponent({
name: 'ATabs',
inheritAttrs: false,
props: initDefaultProps(tabsProps(), {
tabPosition: 'top',
animated: {
inkBar: true,
tabPane: false,
},
}),
slots: [
'tabBarExtraContent',
'leftExtra',
'rightExtra',
'moreIcon',
'addIcon',
'removeIcon',
'renderTabBar',
],
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,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,140 @@
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, { passive: false });
});
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,48 @@
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: Key;
node: VueNode;
}
export type RenderTabBar = (props: { DefaultTabBar: any; [key: string]: any }) => VueNode;
export interface TabsLocale {
dropdownAriaLabel?: string;
removeAriaLabel?: string;
addAriaLabel?: string;
}
export interface EditableConfig {
onEdit: (type: 'add' | 'remove', info: { key?: Key; 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();
position: relative;
display: flex;
overflow: hidden;
.clearfix();
&-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 {
// ========================== Navigation ==========================
> .@{tab-prefix-cls}-nav,
> div > .@{tab-prefix-cls}-nav {
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();
display: flex;
flex: none;
align-items: center;
&-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 {
position: relative;
display: inline-block;
box-sizing: border-box;
margin: 0;
padding-left: 0;
list-style: none;
transition: transform 0.3s @ease-in-out;
&::before,
&::after {
display: table;
content: ' ';
}
&::after {
clear: both;
}
.@{tab-prefix-cls}-tab {
.@{tab-prefix-cls}-nav-wrap {
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;
display: flex;
flex: auto;
align-self: stretch;
overflow: hidden;
white-space: nowrap;
transform: translate(0); // Fix chrome render bug
&::before {
// >>>>> Ping shadow
&::before,
&::after {
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 {
&:active,
&:focus {
color: @tabs-active-color;
}
}
}
.@{iconfont-css-prefix} {
margin-right: 8px;
}
&-extra-content {
flex: none;
}
&-active {
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 {
color: @disabled-color;
cursor: not-allowed;
&-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;
}
}
}
}
.@{tab-prefix-cls}-large-bar {
.@{tab-prefix-cls}-nav-container {
font-size: @tabs-title-font-size-lg;
// ============================ 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;
}
}
.@{tab-prefix-cls}-tab {
padding: @tabs-horizontal-padding-lg;
&-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 &-btn {
color: @tabs-highlight-color;
text-shadow: 0 0 0.25px currentColor;
}
&&-disabled {
color: @disabled-color;
cursor: not-allowed;
}
&&-disabled &-btn,
&&-disabled &-remove {
&:focus,
&:active {
color: @disabled-color;
}
}
& &-remove .@{iconfont-css-prefix} {
margin: 0;
}
.@{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 {
display: flex;
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;
&-animated {
transition: margin @animation-duration-slow;
}
}
// 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;
}
}
.@{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 {
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;
}
}
// 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;
&-tabpane {
flex: none;
width: 100%;
outline: none;
}
}
.@{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,4 +1,4 @@
import type { CSSProperties } from '@vue/runtime-dom';
import type { CSSProperties } from 'vue';
import type { AlignType } from '../vc-align/interface';
import Trigger from '../vc-trigger';
import classNames from '../_util/classNames';

View File

@ -7,7 +7,7 @@ import { setDateTime as setTime } from '../../utils/timeUtil';
import type { PanelRefProps, DisabledTime } from '../../interface';
import KeyCode from '../../../_util/KeyCode';
import classNames from '../../../_util/classNames';
import { ref } from '@vue/reactivity';
import { ref } from 'vue';
import useMergeProps from '../../hooks/useMergeProps';
export type DatetimePanelProps<DateType> = {

View File

@ -1,9 +1,7 @@
import { scrollTo, waitElementReady } from '../../utils/uiUtil';
import { useInjectPanel } from '../../PanelContext';
import classNames from '../../../_util/classNames';
import { ref } from '@vue/reactivity';
import { onBeforeUnmount, watch } from '@vue/runtime-core';
import { defineComponent, nextTick } from 'vue';
import { ref, onBeforeUnmount, watch, defineComponent, nextTick } from 'vue';
export type Unit = {
label: any;

View File

@ -4,7 +4,7 @@ import TimeBody from './TimeBody';
import type { PanelSharedProps, DisabledTimes } from '../../interface';
import { createKeydownHandler } from '../../utils/uiUtil';
import classNames from '../../../_util/classNames';
import { ref } from '@vue/reactivity';
import { ref } from 'vue';
import useMergeProps from '../../hooks/useMergeProps';
export type SharedTimeProps<DateType> = {

View File

@ -3,7 +3,7 @@ import type { GapPositionType } from './types';
import { propTypes } from './types';
import { computed, defineComponent, ref } from 'vue';
import initDefaultProps from '../../_util/props-util/initDefaultProps';
import { useRef } from '../../_util/hooks/useRef';
import useRefs from '../../_util/hooks/useRefs';
let gradientSeed = 0;
@ -75,7 +75,7 @@ export default defineComponent({
const percentList = computed(() => toArray(props.percent));
const strokeColorList = computed(() => toArray(props.strokeColor));
const [setRef, paths] = useRef();
const [setRef, paths] = useRefs();
useTransitionDuration(paths);
const getStokeList = () => {
@ -111,7 +111,7 @@ export default defineComponent({
class: `${prefixCls}-circle-path`,
style: pathStyle,
};
return <path ref={c => setRef(c, index)} {...pathProps} />;
return <path ref={setRef(index)} {...pathProps} />;
});
};

View File

@ -1,4 +1,4 @@
import { useRef } from '../../_util/hooks/useRef';
import useRefs from '../../_util/hooks/useRefs';
import { computed, defineComponent } from 'vue';
import initDefaultProps from '../../_util/props-util/initDefaultProps';
import { useTransitionDuration, defaultProps } from './common';
@ -58,7 +58,7 @@ export default defineComponent({
const { strokeColor } = props;
return Array.isArray(strokeColor) ? strokeColor : [strokeColor];
});
const [setRef, paths] = useRef();
const [setRef, paths] = useRefs();
useTransitionDuration(paths);
const center = computed(() => props.strokeWidth / 2);
const right = computed(() => 100 - props.strokeWidth / 2);
@ -103,7 +103,7 @@ export default defineComponent({
>
<path {...pathFirst.value} />
{percentListProps.value.map((pathProps, index) => {
return <path ref={c => setRef(c, index)} {...pathProps} />;
return <path ref={setRef(index)} {...pathProps} />;
})}
</svg>
);

View File

@ -1,4 +1,5 @@
import type { Refs } from '../../_util/hooks/useRef';
import type { RefsValue } from '../../_util/hooks/useRefs';
import type { Ref } from 'vue';
import { ref, onUpdated } from 'vue';
import type { ProgressProps } from './types';
@ -12,15 +13,15 @@ export const defaultProps: Partial<ProgressProps> = {
trailWidth: 1,
};
export const useTransitionDuration = (paths: Refs) => {
export const useTransitionDuration = (paths: Ref<RefsValue>) => {
const prevTimeStamp = ref(null);
onUpdated(() => {
const now = Date.now();
let updated = false;
Object.keys(paths.value).forEach(key => {
const path = paths.value[key];
paths.value.forEach(val => {
const path = (val as any)?.$el || val;
if (!path) {
return;
}

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,4 +1,4 @@
import type { CSSProperties } from '@vue/runtime-dom';
import type { CSSProperties } from 'vue';
import getScrollBarSize from '../../_util/getScrollBarSize';
import setStyle from '../../_util/setStyle';

View File

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

View File

@ -118,7 +118,6 @@ Array [
"TreeSelectNode",
"Tabs",
"TabPane",
"TabContent",
"Tag",
"CheckableTag",
"TimePicker",