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 deppull/4738/head
parent
022a3ce795
commit
75cf264040
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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>;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
<!---->
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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<{key: string, tab: any, slots: {tab: 'XXX'}}> | - | |
|
||||
| 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<{key: string, tab: any}> | - | |
|
||||
| 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 |
|
||||
|
|
|
@ -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<{key: string, tab: any, slots: {tab: 'XXX'}}> | - | |
|
||||
| tabBarExtraContent | tab bar 上额外的元素 | slot | 无 | 1.5.0 |
|
||||
| tabList | 页签标题列表, 可以通过 customTab(v3.0) 插槽自定义 tab | Array<{key: string, tab: any}> | - | |
|
||||
| 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 | 卡片封面 | - | |
|
||||
|
||||
### 事件
|
||||
|
||||
| 事件名称 | 说明 | 回调参数 | 版本 |
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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-->
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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' },
|
||||
]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 | - |
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 | 选项卡头显示文字 | - |
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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} />
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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];
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 属性,
|
||||
// 添加key之后,会在babel jsx 插件中做一次merge,最终TabBar接收的是一个新的对象,而不是 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} />;
|
||||
},
|
||||
});
|
|
@ -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}
|
||||
|
|
|
@ -10,6 +10,7 @@ export default defineComponent({
|
|||
props: {
|
||||
component: PropTypes.any,
|
||||
title: PropTypes.any,
|
||||
id: String,
|
||||
},
|
||||
setup(props, { slots, attrs }) {
|
||||
const context = useInjectOverflowContext();
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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> = {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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> = {
|
||||
|
|
|
@ -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} />;
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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')} />;
|
||||
},
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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() {
|
||||
// 每次都new一个新的function,避免子节点不能重新渲染
|
||||
const saveRef = name => this.saveRef(name);
|
||||
const getRef = name => this.getRef(name);
|
||||
return this.children(saveRef, getRef);
|
||||
},
|
||||
};
|
|
@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
};
|
|
@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
};
|
|
@ -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>;
|
||||
},
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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>;
|
||||
},
|
||||
});
|
|
@ -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 };
|
|
@ -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);
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -118,7 +118,6 @@ Array [
|
|||
"TreeSelectNode",
|
||||
"Tabs",
|
||||
"TabPane",
|
||||
"TabContent",
|
||||
"Tag",
|
||||
"CheckableTag",
|
||||
"TimePicker",
|
||||
|
|
Loading…
Reference in New Issue