refactor: tabs
parent
022a3ce795
commit
53625976b8
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -686,17 +686,24 @@
|
|||
@tabs-card-head-background: @background-color-light;
|
||||
@tabs-card-height: 40px;
|
||||
@tabs-card-active-color: @primary-color;
|
||||
@tabs-card-horizontal-padding: (
|
||||
(@tabs-card-height - floor(@font-size-base * @line-height-base)) / 2
|
||||
) - @border-width-base @padding-md;
|
||||
@tabs-card-horizontal-padding-sm: 6px @padding-md;
|
||||
@tabs-card-horizontal-padding-lg: 7px @padding-md 6px;
|
||||
@tabs-title-font-size: @font-size-base;
|
||||
@tabs-title-font-size-lg: @font-size-lg;
|
||||
@tabs-title-font-size-sm: @font-size-base;
|
||||
@tabs-ink-bar-color: @primary-color;
|
||||
@tabs-bar-margin: 0 0 16px 0;
|
||||
@tabs-horizontal-margin: 0 32px 0 0;
|
||||
@tabs-horizontal-padding: 12px 16px;
|
||||
@tabs-horizontal-padding-lg: 16px;
|
||||
@tabs-horizontal-padding-sm: 8px 16px;
|
||||
@tabs-vertical-padding: 8px 24px;
|
||||
@tabs-vertical-margin: 0 0 16px 0;
|
||||
@tabs-bar-margin: 0 0 @margin-md 0;
|
||||
@tabs-horizontal-gutter: 32px;
|
||||
@tabs-horizontal-margin: 0 0 0 @tabs-horizontal-gutter;
|
||||
@tabs-horizontal-margin-rtl: 0 0 0 32px;
|
||||
@tabs-horizontal-padding: @padding-sm 0;
|
||||
@tabs-horizontal-padding-lg: @padding-md 0;
|
||||
@tabs-horizontal-padding-sm: @padding-xs 0;
|
||||
@tabs-vertical-padding: @padding-xs @padding-lg;
|
||||
@tabs-vertical-margin: @margin-md 0 0 0;
|
||||
@tabs-scrolling-size: 32px;
|
||||
@tabs-highlight-color: @primary-color;
|
||||
@tabs-hover-color: @primary-5;
|
||||
|
|
|
@ -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;
|
|
@ -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 };
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import type { Tab } from './interface';
|
||||
import type { PropType, InjectionKey } from 'vue';
|
||||
import { provide, inject, defineComponent } from 'vue';
|
||||
|
||||
export interface TabContextProps {
|
||||
tabs: Tab[];
|
||||
prefixCls: string;
|
||||
}
|
||||
|
||||
const TabsContextKey: InjectionKey<TabContextProps> = Symbol('tabsContextKey');
|
||||
|
||||
export const useProvideTabs = (props: TabContextProps) => {
|
||||
provide(TabsContextKey, props);
|
||||
};
|
||||
|
||||
export const useInjectTabs = () => {
|
||||
return inject(TabsContextKey, { tabs: [], prefixCls: undefined });
|
||||
};
|
||||
|
||||
const TabsContextProvider = defineComponent({
|
||||
name: 'TabsContextProvider',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
tabs: { type: Object as PropType<TabContextProps['tabs']>, default: undefined },
|
||||
prefixCls: { type: String, default: undefined },
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
useProvideTabs(props);
|
||||
return () => slots.default?.();
|
||||
},
|
||||
});
|
||||
|
||||
export default TabsContextProvider;
|
|
@ -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,217 @@
|
|||
import Menu, { MenuItem } from '../../../menu';
|
||||
import Dropdown from '../../../vc-dropdown';
|
||||
import type { Tab, TabsLocale, EditableConfig } from '../interface';
|
||||
import AddButton from './AddButton';
|
||||
import type { Key, VueNode } from '../../../_util/type';
|
||||
import KeyCode from '../../../_util/KeyCode';
|
||||
import type { CSSProperties, PropType } from 'vue';
|
||||
import classNames from '../../../_util/classNames';
|
||||
import { defineComponent, watch, computed, onMounted } from 'vue';
|
||||
import PropTypes from '../../../_util/vue-types';
|
||||
import useState from '../../../_util/hooks/useState';
|
||||
|
||||
export interface OperationNodeProps {
|
||||
prefixCls: string;
|
||||
id: string;
|
||||
tabs: Tab[];
|
||||
rtl: boolean;
|
||||
tabBarGutter?: number;
|
||||
activeKey: string;
|
||||
mobile: boolean;
|
||||
moreIcon?: VueNode;
|
||||
moreTransitionName?: string;
|
||||
editable?: EditableConfig;
|
||||
locale?: TabsLocale;
|
||||
onTabClick: (key: Key, e: MouseEvent | KeyboardEvent) => void;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OperationNode',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
prefixCls: { type: String },
|
||||
id: { type: String },
|
||||
tabs: { type: Object as PropType<Tab[]> },
|
||||
rtl: { type: Boolean },
|
||||
tabBarGutter: { type: Number },
|
||||
activeKey: { type: String },
|
||||
mobile: { type: Boolean },
|
||||
moreIcon: PropTypes.any,
|
||||
moreTransitionName: { type: String },
|
||||
editable: { type: Object as PropType<EditableConfig> },
|
||||
locale: { type: Object as PropType<TabsLocale>, default: undefined as TabsLocale },
|
||||
onTabClick: { type: Function as PropType<(key: Key, e: MouseEvent | KeyboardEvent) => void> },
|
||||
},
|
||||
emits: ['tabClick'],
|
||||
slots: ['moreIcon'],
|
||||
setup(props, { attrs, slots }) {
|
||||
// ======================== Dropdown ========================
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedKey, setSelectedKey] = useState<string>(null);
|
||||
const selectOffset = (offset: -1 | 1) => {
|
||||
const enabledTabs = props.tabs.filter(tab => !tab.disabled);
|
||||
let selectedIndex = enabledTabs.findIndex(tab => tab.key === selectedKey.value) || 0;
|
||||
const len = enabledTabs.length;
|
||||
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
selectedIndex = (selectedIndex + offset + len) % len;
|
||||
const tab = enabledTabs[selectedIndex];
|
||||
if (!tab.disabled) {
|
||||
setSelectedKey(tab.key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
const { which } = e;
|
||||
|
||||
if (!open.value) {
|
||||
if ([KeyCode.DOWN, KeyCode.SPACE, KeyCode.ENTER].includes(which)) {
|
||||
setOpen(true);
|
||||
e.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (which) {
|
||||
case KeyCode.UP:
|
||||
selectOffset(-1);
|
||||
e.preventDefault();
|
||||
break;
|
||||
case KeyCode.DOWN:
|
||||
selectOffset(1);
|
||||
e.preventDefault();
|
||||
break;
|
||||
case KeyCode.ESC:
|
||||
setOpen(false);
|
||||
break;
|
||||
case KeyCode.SPACE:
|
||||
case KeyCode.ENTER:
|
||||
if (selectedKey.value !== null) props.onTabClick(selectedKey.value, e);
|
||||
break;
|
||||
}
|
||||
};
|
||||
const popupId = computed(() => `${props.id}-more-popup`);
|
||||
|
||||
const selectedItemId = computed(() =>
|
||||
selectedKey.value !== null ? `${popupId.value}-${selectedKey.value}` : null,
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
watch(
|
||||
selectedKey,
|
||||
() => {
|
||||
const ele = document.getElementById(selectedItemId.value);
|
||||
if (ele && ele.scrollIntoView) {
|
||||
ele.scrollIntoView(false);
|
||||
}
|
||||
},
|
||||
{ flush: 'post', immediate: true },
|
||||
);
|
||||
});
|
||||
|
||||
watch(open, () => {
|
||||
if (!open.value) {
|
||||
setSelectedKey(null);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
const {
|
||||
prefixCls,
|
||||
id,
|
||||
tabs,
|
||||
locale,
|
||||
mobile,
|
||||
moreIcon = slots.moreIcon?.() || 'More',
|
||||
moreTransitionName,
|
||||
editable,
|
||||
tabBarGutter,
|
||||
rtl,
|
||||
onTabClick,
|
||||
} = props;
|
||||
const dropdownPrefix = `${prefixCls}-dropdown`;
|
||||
|
||||
const dropdownAriaLabel = locale?.dropdownAriaLabel;
|
||||
|
||||
// ========================= Render =========================
|
||||
const moreStyle: CSSProperties = {
|
||||
[rtl ? 'marginRight' : 'marginLeft']: tabBarGutter,
|
||||
};
|
||||
if (!tabs.length) {
|
||||
moreStyle.visibility = 'hidden';
|
||||
moreStyle.order = 1;
|
||||
}
|
||||
|
||||
const overlayClassName = classNames({
|
||||
[`${dropdownPrefix}-rtl`]: rtl,
|
||||
});
|
||||
const moreNode = mobile ? null : (
|
||||
<Dropdown
|
||||
prefixCls={dropdownPrefix}
|
||||
trigger={['hover']}
|
||||
visible={open.value}
|
||||
transitionName={moreTransitionName}
|
||||
onVisibleChange={setOpen}
|
||||
overlayClassName={overlayClassName}
|
||||
mouseEnterDelay={0.1}
|
||||
mouseLeaveDelay={0.1}
|
||||
v-slots={{
|
||||
overlay: () => (
|
||||
<Menu
|
||||
onClick={({ key, domEvent }) => {
|
||||
onTabClick(key, domEvent);
|
||||
setOpen(false);
|
||||
}}
|
||||
id={popupId.value}
|
||||
tabindex={-1}
|
||||
role="listbox"
|
||||
aria-activedescendant={selectedItemId.value}
|
||||
selectedKeys={[selectedKey.value]}
|
||||
aria-label={
|
||||
dropdownAriaLabel !== undefined ? dropdownAriaLabel : 'expanded dropdown'
|
||||
}
|
||||
>
|
||||
{tabs.map(tab => (
|
||||
<MenuItem
|
||||
key={tab.key}
|
||||
id={`${popupId.value}-${tab.key}`}
|
||||
role="option"
|
||||
aria-controls={id && `${id}-panel-${tab.key}`}
|
||||
disabled={tab.disabled}
|
||||
>
|
||||
{typeof tab.tab === 'function' ? tab.tab() : tab.tab}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
),
|
||||
default: () => (
|
||||
<button
|
||||
type="button"
|
||||
class={`${prefixCls}-nav-more`}
|
||||
style={moreStyle}
|
||||
tabindex={-1}
|
||||
aria-hidden="true"
|
||||
aria-haspopup="listbox"
|
||||
aria-controls={popupId.value}
|
||||
id={`${id}-more`}
|
||||
aria-expanded={open.value}
|
||||
onKeydown={onKeyDown}
|
||||
>
|
||||
{moreIcon}
|
||||
</button>
|
||||
),
|
||||
}}
|
||||
></Dropdown>
|
||||
);
|
||||
|
||||
return (
|
||||
<div class={classNames(`${prefixCls}-nav-operations`, attrs.class)} style={attrs.style}>
|
||||
{moreNode}
|
||||
<AddButton prefixCls={prefixCls} locale={locale} editable={editable} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,138 @@
|
|||
import type { Tab, EditableConfig } from '../interface';
|
||||
import type { PropType } from 'vue';
|
||||
import { onBeforeUnmount, defineComponent, computed, ref } from 'vue';
|
||||
import type { FocusEventHandler } from '../../../_util/EventInterface';
|
||||
import KeyCode from '../../../_util/KeyCode';
|
||||
import classNames from '../../../_util/classNames';
|
||||
|
||||
export interface TabNodeProps {
|
||||
id: string;
|
||||
prefixCls: string;
|
||||
tab: Tab;
|
||||
active: boolean;
|
||||
closable?: boolean;
|
||||
editable?: EditableConfig;
|
||||
onClick?: (e: MouseEvent | KeyboardEvent) => void;
|
||||
onResize?: (width: number, height: number, left: number, top: number) => void;
|
||||
renderWrapper?: (node: any) => any;
|
||||
removeAriaLabel?: string;
|
||||
onRemove: () => void;
|
||||
onFocus: FocusEventHandler;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TabNode',
|
||||
props: {
|
||||
id: { type: String as PropType<string> },
|
||||
prefixCls: { type: String as PropType<string> },
|
||||
tab: { type: Object as PropType<Tab> },
|
||||
active: { type: Boolean },
|
||||
closable: { type: Boolean },
|
||||
editable: { type: Object as PropType<EditableConfig> },
|
||||
onClick: { type: Function as PropType<(e: MouseEvent | KeyboardEvent) => void> },
|
||||
onResize: {
|
||||
type: Function as PropType<
|
||||
(width: number, height: number, left: number, top: number) => void
|
||||
>,
|
||||
},
|
||||
renderWrapper: { type: Function as PropType<(node: any) => any> },
|
||||
removeAriaLabel: { type: String },
|
||||
onRemove: { type: Function as PropType<() => void> },
|
||||
onFocus: { type: Function as PropType<FocusEventHandler> },
|
||||
},
|
||||
emits: ['click', 'resize', 'remove', 'focus'],
|
||||
setup(props, { expose, attrs }) {
|
||||
const domRef = ref();
|
||||
function onInternalClick(e: MouseEvent | KeyboardEvent) {
|
||||
if (props.tab?.disabled) {
|
||||
return;
|
||||
}
|
||||
props.onClick(e);
|
||||
}
|
||||
expose({
|
||||
domRef,
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
props.onRemove();
|
||||
});
|
||||
function onRemoveTab(event: MouseEvent | KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
props.editable.onEdit('remove', {
|
||||
key: props.tab?.key,
|
||||
event,
|
||||
});
|
||||
}
|
||||
|
||||
const removable = computed(
|
||||
() => props.editable && props.closable !== false && !props.tab?.disabled,
|
||||
);
|
||||
return () => {
|
||||
const {
|
||||
prefixCls,
|
||||
id,
|
||||
active,
|
||||
tab: { key, tab, disabled, closeIcon },
|
||||
renderWrapper,
|
||||
removeAriaLabel,
|
||||
editable,
|
||||
onFocus,
|
||||
} = props;
|
||||
const tabPrefix = `${prefixCls}-tab`;
|
||||
const node = (
|
||||
<div
|
||||
key={key}
|
||||
ref={domRef}
|
||||
class={classNames(tabPrefix, {
|
||||
[`${tabPrefix}-with-remove`]: removable.value,
|
||||
[`${tabPrefix}-active`]: active,
|
||||
[`${tabPrefix}-disabled`]: disabled,
|
||||
})}
|
||||
style={attrs.style}
|
||||
onClick={onInternalClick}
|
||||
>
|
||||
{/* Primary Tab Button */}
|
||||
<div
|
||||
role="tab"
|
||||
aria-selected={active}
|
||||
id={id && `${id}-tab-${key}`}
|
||||
class={`${tabPrefix}-btn`}
|
||||
aria-controls={id && `${id}-panel-${key}`}
|
||||
aria-disabled={disabled}
|
||||
tabindex={disabled ? null : 0}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onInternalClick(e);
|
||||
}}
|
||||
onKeydown={e => {
|
||||
if ([KeyCode.SPACE, KeyCode.ENTER].includes(e.which)) {
|
||||
e.preventDefault();
|
||||
onInternalClick(e);
|
||||
}
|
||||
}}
|
||||
onFocus={onFocus}
|
||||
>
|
||||
{tab}
|
||||
</div>
|
||||
|
||||
{/* Remove Button */}
|
||||
{removable.value && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={removeAriaLabel || 'remove'}
|
||||
tabindex={0}
|
||||
class={`${tabPrefix}-remove`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onRemoveTab(e);
|
||||
}}
|
||||
>
|
||||
{closeIcon || editable.removeIcon || '×'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return renderWrapper ? renderWrapper(node) : node;
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,559 @@
|
|||
import { useRafState } from '../hooks/useRaf';
|
||||
import TabNode from './TabNode';
|
||||
import type {
|
||||
TabSizeMap,
|
||||
TabPosition,
|
||||
RenderTabBar,
|
||||
TabsLocale,
|
||||
EditableConfig,
|
||||
AnimatedConfig,
|
||||
OnTabScroll,
|
||||
TabBarExtraPosition,
|
||||
TabBarExtraContent,
|
||||
} from '../interface';
|
||||
import useOffsets from '../hooks/useOffsets';
|
||||
import OperationNode from './OperationNode';
|
||||
import { useInjectTabs } from '../TabContext';
|
||||
import useTouchMove from '../hooks/useTouchMove';
|
||||
import useRefs from '../hooks/useRefs';
|
||||
import AddButton from './AddButton';
|
||||
import type { Key } from '../../../_util/type';
|
||||
import type { ExtractPropTypes, PropType, CSSProperties } from 'vue';
|
||||
import { onBeforeUnmount, defineComponent, ref, watch, watchEffect, computed } from 'vue';
|
||||
import PropTypes from '../../../_util/vue-types';
|
||||
import useSyncState from '../hooks/useSyncState';
|
||||
import useState from '../../../_util/hooks/useState';
|
||||
import wrapperRaf from '../../../_util/raf';
|
||||
import classNames from '../../../_util/classNames';
|
||||
import ResizeObserver from '../../../vc-resize-observer';
|
||||
import { toPx } from '../../../_util/util';
|
||||
const DEFAULT_SIZE = { width: 0, height: 0, left: 0, top: 0, right: 0 };
|
||||
const tabNavListProps = () => {
|
||||
return {
|
||||
id: { type: String },
|
||||
tabPosition: { type: String as PropType<TabPosition> },
|
||||
activeKey: { type: String },
|
||||
rtl: { type: Boolean },
|
||||
panes: PropTypes.any,
|
||||
animated: { type: Object as PropType<AnimatedConfig>, default: undefined as AnimatedConfig },
|
||||
extra: PropTypes.any,
|
||||
editable: { type: Object as PropType<EditableConfig> },
|
||||
moreIcon: PropTypes.any,
|
||||
moreTransitionName: { type: String },
|
||||
mobile: { type: Boolean },
|
||||
tabBarGutter: { type: Number },
|
||||
renderTabBar: { type: Function as PropType<RenderTabBar> },
|
||||
locale: { type: Object as PropType<TabsLocale>, default: undefined as TabsLocale },
|
||||
onTabClick: {
|
||||
type: Function as PropType<(activeKey: Key, e: MouseEvent | KeyboardEvent) => void>,
|
||||
},
|
||||
onTabScroll: { type: Function as PropType<OnTabScroll> },
|
||||
};
|
||||
};
|
||||
|
||||
export type TabNavListProps = Partial<ExtractPropTypes<ReturnType<typeof tabNavListProps>>>;
|
||||
|
||||
interface ExtraContentProps {
|
||||
position: TabBarExtraPosition;
|
||||
prefixCls: string;
|
||||
extra?: TabBarExtraContent;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TabNavList',
|
||||
inheritAttrs: false,
|
||||
props: tabNavListProps(),
|
||||
slots: ['panes', 'moreIcon', 'extra'],
|
||||
emits: ['tabClick', 'tabScroll'],
|
||||
setup(props, { attrs, slots }) {
|
||||
const tabsContext = useInjectTabs();
|
||||
const tabsWrapperRef = ref<HTMLDivElement>();
|
||||
const tabListRef = ref<HTMLDivElement>();
|
||||
const operationsRef = ref<{ $el: HTMLDivElement }>();
|
||||
const innerAddButtonRef = ref<HTMLButtonElement>();
|
||||
const [getBtnRef, removeBtnRef] = useRefs();
|
||||
|
||||
const tabPositionTopOrBottom = computed(
|
||||
() => props.tabPosition === 'top' || props.tabPosition === 'bottom',
|
||||
);
|
||||
|
||||
const [transformLeft, setTransformLeft] = useSyncState(0, (next, prev) => {
|
||||
if (tabPositionTopOrBottom.value && props.onTabScroll) {
|
||||
props.onTabScroll({ direction: next > prev ? 'left' : 'right' });
|
||||
}
|
||||
});
|
||||
const [transformTop, setTransformTop] = useSyncState(0, (next, prev) => {
|
||||
if (!tabPositionTopOrBottom.value && props.onTabScroll) {
|
||||
props.onTabScroll({ direction: next > prev ? 'top' : 'bottom' });
|
||||
}
|
||||
});
|
||||
|
||||
const [wrapperScrollWidth, setWrapperScrollWidth] = useState<number>(0);
|
||||
const [wrapperScrollHeight, setWrapperScrollHeight] = useState<number>(0);
|
||||
const [wrapperContentWidth, setWrapperContentWidth] = useState<number>(0);
|
||||
const [wrapperContentHeight, setWrapperContentHeight] = useState<number>(0);
|
||||
const [wrapperWidth, setWrapperWidth] = useState<number>(null);
|
||||
const [wrapperHeight, setWrapperHeight] = useState<number>(null);
|
||||
const [addWidth, setAddWidth] = useState<number>(0);
|
||||
const [addHeight, setAddHeight] = useState<number>(0);
|
||||
|
||||
const [tabSizes, setTabSizes] = useRafState<TabSizeMap>(new Map());
|
||||
const tabOffsets = useOffsets(
|
||||
computed(() => tabsContext.tabs),
|
||||
tabSizes,
|
||||
);
|
||||
|
||||
// ========================== Util =========================
|
||||
const operationsHiddenClassName = computed(
|
||||
() => `${tabsContext.prefixCls}-nav-operations-hidden`,
|
||||
);
|
||||
|
||||
const transformMin = ref(0);
|
||||
const transformMax = ref(0);
|
||||
|
||||
watchEffect(() => {
|
||||
if (!tabPositionTopOrBottom.value) {
|
||||
transformMin.value = Math.min(0, wrapperHeight.value - wrapperScrollHeight.value);
|
||||
transformMax.value = 0;
|
||||
} else if (props.rtl) {
|
||||
transformMin.value = 0;
|
||||
transformMax.value = Math.max(0, wrapperScrollWidth.value - wrapperWidth.value);
|
||||
} else {
|
||||
transformMin.value = Math.min(0, wrapperWidth.value - wrapperScrollWidth.value);
|
||||
transformMax.value = 0;
|
||||
}
|
||||
});
|
||||
|
||||
const alignInRange = (value: number): number => {
|
||||
if (value < transformMin.value) {
|
||||
return transformMin.value;
|
||||
}
|
||||
if (value > transformMax.value) {
|
||||
return transformMax.value;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
// ========================= Mobile ========================
|
||||
const touchMovingRef = ref<number>();
|
||||
const [lockAnimation, setLockAnimation] = useState<number>();
|
||||
|
||||
const doLockAnimation = () => {
|
||||
setLockAnimation(Date.now());
|
||||
};
|
||||
|
||||
const clearTouchMoving = () => {
|
||||
window.clearTimeout(touchMovingRef.value);
|
||||
};
|
||||
const doMove = (setState: (fn: (val: number) => number) => void, offset: number) => {
|
||||
setState((value: number) => {
|
||||
const newValue = alignInRange(value + offset);
|
||||
|
||||
return newValue;
|
||||
});
|
||||
};
|
||||
useTouchMove(tabsWrapperRef, (offsetX, offsetY) => {
|
||||
if (tabPositionTopOrBottom.value) {
|
||||
// Skip scroll if place is enough
|
||||
if (wrapperWidth.value >= wrapperScrollWidth.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
doMove(setTransformLeft, offsetX);
|
||||
} else {
|
||||
if (wrapperHeight.value >= wrapperScrollHeight.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
doMove(setTransformTop, offsetY);
|
||||
}
|
||||
|
||||
clearTouchMoving();
|
||||
doLockAnimation();
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
watch(lockAnimation, () => {
|
||||
clearTouchMoving();
|
||||
if (lockAnimation.value) {
|
||||
touchMovingRef.value = window.setTimeout(() => {
|
||||
setLockAnimation(0);
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// ========================= Scroll ========================
|
||||
const scrollToTab = (key = props.activeKey) => {
|
||||
const tabOffset = tabOffsets.value.get(key) || {
|
||||
width: 0,
|
||||
height: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
};
|
||||
|
||||
if (tabPositionTopOrBottom.value) {
|
||||
// ============ Align with top & bottom ============
|
||||
let newTransform = transformLeft.value;
|
||||
|
||||
// RTL
|
||||
if (props.rtl) {
|
||||
if (tabOffset.right < transformLeft.value) {
|
||||
newTransform = tabOffset.right;
|
||||
} else if (tabOffset.right + tabOffset.width > transformLeft.value + wrapperWidth.value) {
|
||||
newTransform = tabOffset.right + tabOffset.width - wrapperWidth.value;
|
||||
}
|
||||
}
|
||||
// LTR
|
||||
else if (tabOffset.left < -transformLeft.value) {
|
||||
newTransform = -tabOffset.left;
|
||||
} else if (tabOffset.left + tabOffset.width > -transformLeft.value + wrapperWidth.value) {
|
||||
newTransform = -(tabOffset.left + tabOffset.width - wrapperWidth.value);
|
||||
}
|
||||
|
||||
setTransformTop(0);
|
||||
setTransformLeft(alignInRange(newTransform));
|
||||
} else {
|
||||
// ============ Align with left & right ============
|
||||
let newTransform = transformTop.value;
|
||||
|
||||
if (tabOffset.top < -transformTop.value) {
|
||||
newTransform = -tabOffset.top;
|
||||
} else if (tabOffset.top + tabOffset.height > -transformTop.value + wrapperHeight.value) {
|
||||
newTransform = -(tabOffset.top + tabOffset.height - wrapperHeight.value);
|
||||
}
|
||||
|
||||
setTransformLeft(0);
|
||||
setTransformTop(alignInRange(newTransform));
|
||||
}
|
||||
};
|
||||
|
||||
const visibleStart = ref(0);
|
||||
const visibleEnd = ref(0);
|
||||
|
||||
watchEffect(() => {
|
||||
let unit: 'width' | 'height';
|
||||
let position: 'left' | 'top' | 'right';
|
||||
let transformSize: number;
|
||||
let basicSize: number;
|
||||
let tabContentSize: number;
|
||||
let addSize: number;
|
||||
|
||||
if (['top', 'bottom'].includes(props.tabPosition)) {
|
||||
unit = 'width';
|
||||
basicSize = wrapperContentWidth.value;
|
||||
tabContentSize = wrapperContentWidth.value;
|
||||
addSize = addWidth.value;
|
||||
position = props.rtl ? 'right' : 'left';
|
||||
transformSize = Math.abs(transformLeft.value);
|
||||
} else {
|
||||
unit = 'height';
|
||||
basicSize = wrapperContentHeight.value;
|
||||
tabContentSize = wrapperContentHeight.value;
|
||||
addSize = addHeight.value;
|
||||
position = 'top';
|
||||
transformSize = -transformTop.value;
|
||||
}
|
||||
|
||||
let mergedBasicSize = basicSize;
|
||||
if (tabContentSize + addSize > basicSize) {
|
||||
mergedBasicSize = basicSize - addSize;
|
||||
}
|
||||
|
||||
const { tabs } = tabsContext;
|
||||
if (!tabs.length) {
|
||||
[visibleStart.value, visibleEnd.value] = [0, 0];
|
||||
}
|
||||
|
||||
const len = tabs.length;
|
||||
let endIndex = len;
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
const offset = tabOffsets.value.get(tabs[i].key) || DEFAULT_SIZE;
|
||||
if (offset[position] + offset[unit] > transformSize + mergedBasicSize) {
|
||||
endIndex = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let startIndex = 0;
|
||||
for (let i = len - 1; i >= 0; i -= 1) {
|
||||
const offset = tabOffsets.value.get(tabs[i].key) || DEFAULT_SIZE;
|
||||
if (offset[position] < transformSize) {
|
||||
startIndex = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[visibleStart.value, visibleEnd.value] = [startIndex, endIndex];
|
||||
});
|
||||
|
||||
const onListHolderResize = () => {
|
||||
// Update wrapper records
|
||||
const offsetWidth = tabsWrapperRef.value?.offsetWidth || 0;
|
||||
const offsetHeight = tabsWrapperRef.value?.offsetHeight || 0;
|
||||
const newAddWidth = innerAddButtonRef.value?.offsetWidth || 0;
|
||||
const newAddHeight = innerAddButtonRef.value?.offsetHeight || 0;
|
||||
const newOperationWidth = operationsRef.value?.$el.offsetWidth || 0;
|
||||
const newOperationHeight = operationsRef.value?.$el.offsetHeight || 0;
|
||||
|
||||
setWrapperWidth(offsetWidth);
|
||||
setWrapperHeight(offsetHeight);
|
||||
setAddWidth(newAddWidth);
|
||||
setAddHeight(newAddHeight);
|
||||
|
||||
const newWrapperScrollWidth = (tabListRef.value?.offsetWidth || 0) - newAddWidth;
|
||||
const newWrapperScrollHeight = (tabListRef.value?.offsetHeight || 0) - newAddHeight;
|
||||
|
||||
setWrapperScrollWidth(newWrapperScrollWidth);
|
||||
setWrapperScrollHeight(newWrapperScrollHeight);
|
||||
|
||||
const isOperationHidden = operationsRef.value?.$el.className.includes(
|
||||
operationsHiddenClassName.value,
|
||||
);
|
||||
setWrapperContentWidth(newWrapperScrollWidth - (isOperationHidden ? 0 : newOperationWidth));
|
||||
setWrapperContentHeight(
|
||||
newWrapperScrollHeight - (isOperationHidden ? 0 : newOperationHeight),
|
||||
);
|
||||
|
||||
// Update buttons records
|
||||
setTabSizes(() => {
|
||||
const newSizes: TabSizeMap = new Map();
|
||||
tabsContext.tabs.forEach(({ key }) => {
|
||||
const btnRef = getBtnRef(key).value;
|
||||
const btnNode = (btnRef as any).$el || btnRef;
|
||||
if (btnNode) {
|
||||
newSizes.set(key, {
|
||||
width: btnNode.offsetWidth,
|
||||
height: btnNode.offsetHeight,
|
||||
left: btnNode.offsetLeft,
|
||||
top: btnNode.offsetTop,
|
||||
});
|
||||
}
|
||||
});
|
||||
return newSizes;
|
||||
});
|
||||
};
|
||||
|
||||
// ======================== Dropdown =======================
|
||||
const hiddenTabs = computed(() => [
|
||||
...tabsContext.tabs.slice(0, visibleStart.value),
|
||||
...tabsContext.tabs.slice(visibleEnd.value + 1),
|
||||
]);
|
||||
|
||||
// =================== Link & Operations ===================
|
||||
const [inkStyle, setInkStyle] = useState<CSSProperties>();
|
||||
|
||||
const activeTabOffset = computed(() => tabOffsets.value.get(props.activeKey));
|
||||
|
||||
// Delay set ink style to avoid remove tab blink
|
||||
const inkBarRafRef = ref<number>();
|
||||
const cleanInkBarRaf = () => {
|
||||
wrapperRaf.cancel(inkBarRafRef.value);
|
||||
};
|
||||
|
||||
watch([activeTabOffset, tabPositionTopOrBottom, () => props.rtl], () => {
|
||||
const newInkStyle: CSSProperties = {};
|
||||
|
||||
if (activeTabOffset.value) {
|
||||
if (tabPositionTopOrBottom.value) {
|
||||
if (props.rtl) {
|
||||
newInkStyle.right = toPx(activeTabOffset.value.right);
|
||||
} else {
|
||||
newInkStyle.left = toPx(activeTabOffset.value.left);
|
||||
}
|
||||
|
||||
newInkStyle.width = toPx(activeTabOffset.value.width);
|
||||
} else {
|
||||
newInkStyle.top = toPx(activeTabOffset.value.top);
|
||||
newInkStyle.height = toPx(activeTabOffset.value.height);
|
||||
}
|
||||
}
|
||||
|
||||
cleanInkBarRaf();
|
||||
inkBarRafRef.value = wrapperRaf(() => {
|
||||
setInkStyle(newInkStyle);
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
[() => props.activeKey, activeTabOffset, tabOffsets, tabPositionTopOrBottom],
|
||||
() => {
|
||||
scrollToTab();
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
|
||||
watch(
|
||||
[() => props.rtl, () => props.tabBarGutter, () => props.activeKey, () => tabsContext.tabs],
|
||||
() => {
|
||||
onListHolderResize();
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
|
||||
const ExtraContent = ({ position, prefixCls, extra }: ExtraContentProps) => {
|
||||
if (!extra) return null;
|
||||
|
||||
const content = slots.extra?.({ position });
|
||||
|
||||
return content ? <div class={`${prefixCls}-extra-content`}>{content}</div> : null;
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTouchMoving();
|
||||
cleanInkBarRaf();
|
||||
});
|
||||
|
||||
return () => {
|
||||
const { prefixCls, tabs } = tabsContext;
|
||||
const {
|
||||
id,
|
||||
animated,
|
||||
activeKey,
|
||||
rtl,
|
||||
extra,
|
||||
editable,
|
||||
locale,
|
||||
tabPosition,
|
||||
tabBarGutter,
|
||||
onTabClick,
|
||||
} = props;
|
||||
const { class: className, style } = attrs;
|
||||
// ========================= Render ========================
|
||||
const hasDropdown = !!hiddenTabs.value.length;
|
||||
const wrapPrefix = `${prefixCls}-nav-wrap`;
|
||||
let pingLeft: boolean;
|
||||
let pingRight: boolean;
|
||||
let pingTop: boolean;
|
||||
let pingBottom: boolean;
|
||||
|
||||
if (tabPositionTopOrBottom.value) {
|
||||
if (rtl) {
|
||||
pingRight = transformLeft.value > 0;
|
||||
pingLeft = transformLeft.value + wrapperWidth.value < wrapperScrollWidth.value;
|
||||
} else {
|
||||
pingLeft = transformLeft.value < 0;
|
||||
pingRight = -transformLeft.value + wrapperWidth.value < wrapperScrollWidth.value;
|
||||
}
|
||||
} else {
|
||||
pingTop = transformTop.value < 0;
|
||||
pingBottom = -transformTop.value + wrapperHeight.value < wrapperScrollHeight.value;
|
||||
}
|
||||
|
||||
const tabNodeStyle: CSSProperties = {};
|
||||
if (tabPosition === 'top' || tabPosition === 'bottom') {
|
||||
tabNodeStyle[rtl ? 'marginRight' : 'marginLeft'] =
|
||||
typeof tabBarGutter === 'number' ? `${tabBarGutter}px` : tabBarGutter;
|
||||
} else {
|
||||
tabNodeStyle.marginTop =
|
||||
typeof tabBarGutter === 'number' ? `${tabBarGutter}px` : tabBarGutter;
|
||||
}
|
||||
|
||||
const tabNodes = tabs.map((tab, i) => {
|
||||
const { key } = tab;
|
||||
return (
|
||||
<TabNode
|
||||
id={id}
|
||||
prefixCls={prefixCls}
|
||||
key={key}
|
||||
tab={tab}
|
||||
/* first node should not have margin left */
|
||||
style={i === 0 ? undefined : tabNodeStyle}
|
||||
closable={tab.closable}
|
||||
editable={editable}
|
||||
active={key === activeKey}
|
||||
removeAriaLabel={locale?.removeAriaLabel}
|
||||
ref={getBtnRef(key)}
|
||||
onClick={e => {
|
||||
onTabClick(key, e);
|
||||
}}
|
||||
onRemove={() => {
|
||||
removeBtnRef(key);
|
||||
}}
|
||||
onFocus={() => {
|
||||
scrollToTab(key);
|
||||
doLockAnimation();
|
||||
if (!tabsWrapperRef.value) {
|
||||
return;
|
||||
}
|
||||
// Focus element will make scrollLeft change which we should reset back
|
||||
if (!rtl) {
|
||||
tabsWrapperRef.value.scrollLeft = 0;
|
||||
}
|
||||
tabsWrapperRef.value.scrollTop = 0;
|
||||
}}
|
||||
v-slots={slots}
|
||||
></TabNode>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="tablist"
|
||||
class={classNames(`${prefixCls}-nav`, className)}
|
||||
style={style}
|
||||
onKeydown={() => {
|
||||
// No need animation when use keyboard
|
||||
doLockAnimation();
|
||||
}}
|
||||
>
|
||||
<ExtraContent position="left" extra={extra} prefixCls={prefixCls} />
|
||||
|
||||
<ResizeObserver onResize={onListHolderResize}>
|
||||
<div
|
||||
class={classNames(wrapPrefix, {
|
||||
[`${wrapPrefix}-ping-left`]: pingLeft,
|
||||
[`${wrapPrefix}-ping-right`]: pingRight,
|
||||
[`${wrapPrefix}-ping-top`]: pingTop,
|
||||
[`${wrapPrefix}-ping-bottom`]: pingBottom,
|
||||
})}
|
||||
ref={tabsWrapperRef}
|
||||
>
|
||||
<ResizeObserver onResize={onListHolderResize}>
|
||||
<div
|
||||
ref={tabListRef}
|
||||
class={`${prefixCls}-nav-list`}
|
||||
style={{
|
||||
transform: `translate(${transformLeft.value}px, ${transformTop.value}px)`,
|
||||
transition: lockAnimation.value ? 'none' : undefined,
|
||||
}}
|
||||
>
|
||||
{tabNodes}
|
||||
<AddButton
|
||||
ref={innerAddButtonRef}
|
||||
prefixCls={prefixCls}
|
||||
locale={locale}
|
||||
editable={editable}
|
||||
style={{
|
||||
...(tabNodes.length === 0 ? undefined : tabNodeStyle),
|
||||
visibility: hasDropdown ? 'hidden' : null,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
class={classNames(`${prefixCls}-ink-bar`, {
|
||||
[`${prefixCls}-ink-bar-animated`]: animated.inkBar,
|
||||
})}
|
||||
style={inkStyle.value}
|
||||
/>
|
||||
</div>
|
||||
</ResizeObserver>
|
||||
</div>
|
||||
</ResizeObserver>
|
||||
|
||||
<OperationNode
|
||||
{...props}
|
||||
ref={operationsRef}
|
||||
prefixCls={prefixCls}
|
||||
tabs={hiddenTabs.value}
|
||||
class={!hasDropdown && operationsHiddenClassName.value}
|
||||
/>
|
||||
|
||||
<ExtraContent position="right" extra={extra} prefixCls={prefixCls} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
import { defineComponent, ref, watch, computed } from 'vue';
|
||||
import type { CSSProperties } from 'vue';
|
||||
import type { VueNode } from '../../../_util/type';
|
||||
import PropTypes from '../../../_util/vue-types';
|
||||
|
||||
export interface TabPaneProps {
|
||||
tab?: VueNode | (() => VueNode);
|
||||
disabled?: boolean;
|
||||
forceRender?: boolean;
|
||||
closable?: boolean;
|
||||
closeIcon?: VueNode;
|
||||
|
||||
// Pass by TabPaneList
|
||||
prefixCls?: string;
|
||||
tabKey?: string;
|
||||
id?: string;
|
||||
animated?: boolean;
|
||||
active?: boolean;
|
||||
destroyInactiveTabPane?: boolean;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TabPane',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
tab: PropTypes.any,
|
||||
disabled: { type: Boolean },
|
||||
forceRender: { type: Boolean },
|
||||
closable: { type: Boolean },
|
||||
animated: { type: Boolean },
|
||||
active: { type: Boolean },
|
||||
destroyInactiveTabPane: { type: Boolean },
|
||||
|
||||
// Pass by TabPaneList
|
||||
prefixCls: { type: String },
|
||||
tabKey: { type: String },
|
||||
id: { type: String },
|
||||
},
|
||||
slots: ['closeIcon', 'tab'],
|
||||
setup(props, { attrs, slots }) {
|
||||
const visited = ref(props.forceRender);
|
||||
watch(
|
||||
[() => props.active, () => props.destroyInactiveTabPane],
|
||||
() => {
|
||||
if (props.active) {
|
||||
visited.value = true;
|
||||
} else if (props.destroyInactiveTabPane) {
|
||||
visited.value = false;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const mergedStyle = computed<CSSProperties>(() => {
|
||||
if (!props.active) {
|
||||
if (props.animated) {
|
||||
return {
|
||||
visibility: 'hidden',
|
||||
height: 0,
|
||||
overflowY: 'hidden',
|
||||
};
|
||||
} else {
|
||||
return { display: 'none' };
|
||||
}
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
return () => {
|
||||
const { prefixCls, forceRender, id, active, tabKey } = props;
|
||||
return (
|
||||
<div
|
||||
id={id && `${id}-panel-${tabKey}`}
|
||||
role="tabpanel"
|
||||
tabindex={active ? 0 : -1}
|
||||
aria-labelledby={id && `${id}-tab-${tabKey}`}
|
||||
aria-hidden={!active}
|
||||
style={{ ...mergedStyle.value, ...(attrs.style as any) }}
|
||||
class={[`${prefixCls}-tabpane`, active && `${prefixCls}-tabpane-active`, attrs.class]}
|
||||
>
|
||||
{(active || visited.value || forceRender) && slots.default?.()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
import { useInjectTabs } from '../TabContext';
|
||||
import type { TabPosition, AnimatedConfig } from '../interface';
|
||||
import type { Key } from '../../../_util/type';
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { cloneElement } from '../../../_util/vnode';
|
||||
|
||||
export interface TabPanelListProps {
|
||||
activeKey: Key;
|
||||
id: string;
|
||||
rtl: boolean;
|
||||
animated?: AnimatedConfig;
|
||||
tabPosition?: TabPosition;
|
||||
destroyInactiveTabPane?: boolean;
|
||||
}
|
||||
export default defineComponent({
|
||||
name: 'TabPanelList',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
activeKey: { type: [String, Number] as PropType<Key> },
|
||||
id: { type: String },
|
||||
rtl: { type: Boolean },
|
||||
animated: { type: Object as PropType<AnimatedConfig>, default: undefined as AnimatedConfig },
|
||||
tabPosition: { type: String as PropType<TabPosition> },
|
||||
destroyInactiveTabPane: { type: Boolean },
|
||||
},
|
||||
setup(props) {
|
||||
const tabsContext = useInjectTabs();
|
||||
return () => {
|
||||
const { id, activeKey, animated, tabPosition, rtl, destroyInactiveTabPane } = props;
|
||||
const { prefixCls, tabs } = tabsContext;
|
||||
const tabPaneAnimated = animated.tabPane;
|
||||
|
||||
const activeIndex = tabs.findIndex(tab => tab.key === activeKey);
|
||||
|
||||
return (
|
||||
<div class={`${prefixCls}-content-holder`}>
|
||||
<div
|
||||
class={[
|
||||
`${prefixCls}-content`,
|
||||
`${prefixCls}-content-${tabPosition}`,
|
||||
{
|
||||
[`${prefixCls}-content-animated`]: tabPaneAnimated,
|
||||
},
|
||||
]}
|
||||
style={
|
||||
activeIndex && tabPaneAnimated
|
||||
? { [rtl ? 'marginRight' : 'marginLeft']: `-${activeIndex}00%` }
|
||||
: null
|
||||
}
|
||||
>
|
||||
{tabs.map(tab => {
|
||||
return cloneElement(tab.node, {
|
||||
key: tab.key,
|
||||
prefixCls,
|
||||
tabKey: tab.key,
|
||||
id,
|
||||
animated: tabPaneAnimated,
|
||||
active: tab.key === activeKey,
|
||||
destroyInactiveTabPane,
|
||||
});
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,334 @@
|
|||
// Accessibility https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Tab_Role
|
||||
import TabNavList from './TabNavList';
|
||||
import TabPanelList from './TabPanelList';
|
||||
import type {
|
||||
TabPosition,
|
||||
RenderTabBar,
|
||||
TabsLocale,
|
||||
EditableConfig,
|
||||
AnimatedConfig,
|
||||
OnTabScroll,
|
||||
Tab,
|
||||
} from './interface';
|
||||
import type { CSSProperties, PropType, ExtractPropTypes } from 'vue';
|
||||
import { defineComponent, computed, onMounted, watchEffect, camelize } from 'vue';
|
||||
import { flattenChildren, initDefaultProps, isValidElement } from '../../_util/props-util';
|
||||
import useConfigInject from '../../_util/hooks/useConfigInject';
|
||||
import useState from '../../_util/hooks/useState';
|
||||
import isMobile from '../../vc-util/isMobile';
|
||||
import useMergedState from '../../_util/hooks/useMergedState';
|
||||
import classNames from '../../_util/classNames';
|
||||
import { CloseOutlined, PlusOutlined } from '@ant-design/icons-vue';
|
||||
import devWarning from '../../vc-util/devWarning';
|
||||
import type { SizeType } from '../../config-provider';
|
||||
import TabsContextProvider from './TabContext';
|
||||
|
||||
export type TabsType = 'line' | 'card' | 'editable-card';
|
||||
export type TabsPosition = 'top' | 'right' | 'bottom' | 'left';
|
||||
|
||||
// Used for accessibility
|
||||
let uuid = 0;
|
||||
|
||||
export const tabsProps = () => {
|
||||
return {
|
||||
prefixCls: { type: String },
|
||||
id: { type: String },
|
||||
|
||||
activeKey: { type: String },
|
||||
defaultActiveKey: { type: String },
|
||||
direction: { type: String as PropType<'ltr' | 'rtl'> },
|
||||
animated: { type: [Boolean, Object] as PropType<boolean | AnimatedConfig> },
|
||||
renderTabBar: { type: Function as PropType<RenderTabBar> },
|
||||
tabBarGutter: { type: Number },
|
||||
tabBarStyle: { type: Object as PropType<CSSProperties> },
|
||||
tabPosition: { type: String as PropType<TabPosition> },
|
||||
destroyInactiveTabPane: { type: Boolean },
|
||||
|
||||
hideAdd: Boolean,
|
||||
type: { type: String as PropType<TabsType> },
|
||||
size: { type: String as PropType<SizeType> },
|
||||
centered: Boolean,
|
||||
onEdit: {
|
||||
type: Function as PropType<
|
||||
(e: MouseEvent | KeyboardEvent | string, action: 'add' | 'remove') => void
|
||||
>,
|
||||
},
|
||||
onChange: { type: Function as PropType<(activeKey: string) => void> },
|
||||
onTabClick: {
|
||||
type: Function as PropType<(activeKey: string, e: KeyboardEvent | MouseEvent) => void>,
|
||||
},
|
||||
onTabScroll: { type: Function as PropType<OnTabScroll> },
|
||||
|
||||
// Accessibility
|
||||
locale: { type: Object as PropType<TabsLocale>, default: undefined as TabsLocale },
|
||||
onPrevClick: Function,
|
||||
onNextClick: Function,
|
||||
};
|
||||
};
|
||||
|
||||
export type TabsProps = Partial<ExtractPropTypes<ReturnType<typeof tabsProps>>>;
|
||||
|
||||
function parseTabList(children: any[]): Tab[] {
|
||||
return children
|
||||
.map(node => {
|
||||
if (isValidElement(node)) {
|
||||
const props = { ...(node.props || {}) };
|
||||
for (const [k, v] of Object.entries(props)) {
|
||||
delete props[k];
|
||||
props[camelize(k)] = v;
|
||||
}
|
||||
const slots = node.children || {};
|
||||
const key = node.key !== undefined ? String(node.key) : undefined;
|
||||
const {
|
||||
tab = slots.tab,
|
||||
disabled,
|
||||
forceRender,
|
||||
closable,
|
||||
animated,
|
||||
active,
|
||||
destroyInactiveTabPane,
|
||||
} = props;
|
||||
return {
|
||||
key,
|
||||
...props,
|
||||
node,
|
||||
closeIcon: slots.closeIcon,
|
||||
tab,
|
||||
disabled: disabled === '' || disabled,
|
||||
forceRender: forceRender === '' || forceRender,
|
||||
closable: closable === '' || closable,
|
||||
animated: animated === '' || animated,
|
||||
active: active === '' || active,
|
||||
destroyInactiveTabPane: destroyInactiveTabPane === '' || destroyInactiveTabPane,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter(tab => tab);
|
||||
}
|
||||
const InternalTabs = defineComponent({
|
||||
name: 'InternalTabs',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
...initDefaultProps(tabsProps(), {
|
||||
tabPosition: 'top',
|
||||
animated: {
|
||||
inkBar: true,
|
||||
tabPane: false,
|
||||
},
|
||||
}),
|
||||
tabs: { type: Array as PropType<Tab[]> },
|
||||
},
|
||||
slots: ['tabBarExtraContent', 'moreIcon', 'addIcon', 'removeIcon'],
|
||||
emits: ['tabClick', 'tabScroll', 'change', 'update:activeKey'],
|
||||
setup(props, { attrs, slots }) {
|
||||
devWarning(
|
||||
!(props.onPrevClick !== undefined) && !(props.onNextClick !== undefined),
|
||||
'Tabs',
|
||||
'`onPrevClick / @prevClick` and `onNextClick / @nextClick` has been removed. Please use `onTabScroll / @tabScroll` instead.',
|
||||
);
|
||||
const { prefixCls, direction, size, rootPrefixCls } = useConfigInject('tabs', props);
|
||||
const rtl = computed(() => direction.value === 'rtl');
|
||||
const mergedAnimated = computed<AnimatedConfig>(() => {
|
||||
const { animated } = props;
|
||||
if (animated === false) {
|
||||
return {
|
||||
inkBar: false,
|
||||
tabPane: false,
|
||||
};
|
||||
} else if (animated === true) {
|
||||
return {
|
||||
inkBar: true,
|
||||
tabPane: true,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
inkBar: true,
|
||||
tabPane: false,
|
||||
...(typeof animated === 'object' ? animated : {}),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// ======================== Mobile ========================
|
||||
const [mobile, setMobile] = useState(false);
|
||||
onMounted(() => {
|
||||
// Only update on the client side
|
||||
setMobile(isMobile());
|
||||
});
|
||||
|
||||
// ====================== Active Key ======================
|
||||
const [mergedActiveKey, setMergedActiveKey] = useMergedState<string>(() => props.tabs[0]?.key, {
|
||||
value: computed(() => props.activeKey),
|
||||
defaultValue: props.defaultActiveKey,
|
||||
});
|
||||
const [activeIndex, setActiveIndex] = useState(() =>
|
||||
props.tabs.findIndex(tab => tab.key === mergedActiveKey.value),
|
||||
);
|
||||
|
||||
watchEffect(() => {
|
||||
let newActiveIndex = props.tabs.findIndex(tab => tab.key === mergedActiveKey.value);
|
||||
if (newActiveIndex === -1) {
|
||||
newActiveIndex = Math.max(0, Math.min(activeIndex.value, props.tabs.length - 1));
|
||||
setMergedActiveKey(props.tabs[newActiveIndex]?.key);
|
||||
}
|
||||
setActiveIndex(newActiveIndex);
|
||||
});
|
||||
|
||||
// ===================== Accessibility ====================
|
||||
const [mergedId, setMergedId] = useMergedState(null, {
|
||||
value: computed(() => props.id),
|
||||
});
|
||||
|
||||
const mergedTabPosition = computed(() => {
|
||||
if (mobile.value && !['left', 'right'].includes(props.tabPosition)) {
|
||||
return 'top';
|
||||
} else {
|
||||
return props.tabPosition;
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.id) {
|
||||
setMergedId(`rc-tabs-${process.env.NODE_ENV === 'test' ? 'test' : uuid}`);
|
||||
uuid += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// ======================== Events ========================
|
||||
const onInternalTabClick = (key: string, e: MouseEvent | KeyboardEvent) => {
|
||||
props.onTabClick?.(key, e);
|
||||
|
||||
setMergedActiveKey(key);
|
||||
props.onChange?.(key);
|
||||
};
|
||||
|
||||
return () => {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
activeKey,
|
||||
defaultActiveKey,
|
||||
tabBarGutter,
|
||||
tabBarStyle,
|
||||
locale,
|
||||
destroyInactiveTabPane,
|
||||
renderTabBar,
|
||||
onChange,
|
||||
onTabClick,
|
||||
onTabScroll,
|
||||
hideAdd,
|
||||
centered,
|
||||
...restProps
|
||||
} = props;
|
||||
// ======================== Render ========================
|
||||
const sharedProps = {
|
||||
id: mergedId.value,
|
||||
activeKey: mergedActiveKey.value,
|
||||
animated: mergedAnimated.value,
|
||||
tabPosition: mergedTabPosition.value,
|
||||
rtl: rtl.value,
|
||||
mobile: mobile.value,
|
||||
};
|
||||
|
||||
let editable: EditableConfig | undefined;
|
||||
if (type === 'editable-card') {
|
||||
editable = {
|
||||
onEdit: (editType, { key, event }) => {
|
||||
props.onEdit?.(editType === 'add' ? event : key!, editType);
|
||||
},
|
||||
removeIcon: () => <CloseOutlined />,
|
||||
addIcon: slots.addIcon ? slots.addIcon : () => <PlusOutlined />,
|
||||
showAdd: hideAdd !== true,
|
||||
};
|
||||
}
|
||||
|
||||
let tabNavBar;
|
||||
|
||||
const tabNavBarProps = {
|
||||
...sharedProps,
|
||||
moreTransitionName: `${rootPrefixCls.value}-slide-up`,
|
||||
editable,
|
||||
locale,
|
||||
tabBarGutter,
|
||||
onTabClick: onInternalTabClick,
|
||||
onTabScroll,
|
||||
style: tabBarStyle,
|
||||
panes: flattenChildren(slots.default?.()),
|
||||
};
|
||||
|
||||
if (renderTabBar) {
|
||||
tabNavBar = renderTabBar(tabNavBarProps, TabNavList);
|
||||
} else {
|
||||
tabNavBar = (
|
||||
<TabNavList
|
||||
{...tabNavBarProps}
|
||||
v-slots={{
|
||||
moreIcon: slots.moreIcon,
|
||||
extra: slots.tabBarExtraContent,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const pre = prefixCls.value;
|
||||
|
||||
return (
|
||||
<TabsContextProvider tabs={props.tabs} prefixCls={pre}>
|
||||
<div
|
||||
{...attrs}
|
||||
id={id}
|
||||
class={classNames(
|
||||
pre,
|
||||
`${pre}-${mergedTabPosition.value}`,
|
||||
{
|
||||
[`${pre}-${size}`]: size.value,
|
||||
[`${pre}-card`]: ['card', 'editable-card'].includes(type as string),
|
||||
[`${pre}-editable-card`]: type === 'editable-card',
|
||||
[`${pre}-centered`]: centered,
|
||||
[`${pre}-mobile`]: mobile.value,
|
||||
[`${pre}-editable`]: type === 'editable-card',
|
||||
[`${pre}-rtl`]: rtl.value,
|
||||
},
|
||||
attrs.class,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{tabNavBar}
|
||||
<TabPanelList
|
||||
destroyInactiveTabPane={destroyInactiveTabPane}
|
||||
{...sharedProps}
|
||||
animated={mergedAnimated.value}
|
||||
/>
|
||||
</div>
|
||||
</TabsContextProvider>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ATabs',
|
||||
inheritAttrs: false,
|
||||
props: initDefaultProps(tabsProps(), {
|
||||
tabPosition: 'top',
|
||||
animated: {
|
||||
inkBar: true,
|
||||
tabPane: false,
|
||||
},
|
||||
}),
|
||||
slots: ['tabBarExtraContent', 'moreIcon', 'addIcon', 'removeIcon'],
|
||||
emits: ['tabClick', 'tabScroll', 'change', 'update:activeKey'],
|
||||
setup(props, { attrs, slots, emit }) {
|
||||
const handleChange = (key: string) => {
|
||||
emit('update:activeKey', key);
|
||||
emit('change', key);
|
||||
};
|
||||
return () => {
|
||||
const tabs = parseTabList(flattenChildren(slots.default?.()));
|
||||
return (
|
||||
<InternalTabs {...props} {...attrs} onChange={handleChange} tabs={tabs} v-slots={slots} />
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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,23 @@
|
|||
import type { Ref, ComponentPublicInstance } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import type { Key } from '../../../_util/type';
|
||||
|
||||
export default function useRefs(): [
|
||||
(key: Key) => Ref<HTMLElement | ComponentPublicInstance>,
|
||||
(key: Key) => void,
|
||||
] {
|
||||
const cacheRefs = ref(new Map<Key, Ref<HTMLElement | ComponentPublicInstance>>());
|
||||
|
||||
function getRef(key: Key) {
|
||||
if (!cacheRefs.value.has(key)) {
|
||||
cacheRefs.value.set(key, ref());
|
||||
}
|
||||
return cacheRefs.value.get(key);
|
||||
}
|
||||
|
||||
function removeRef(key: Key) {
|
||||
cacheRefs.value.delete(key);
|
||||
}
|
||||
|
||||
return [getRef, removeRef];
|
||||
}
|
|
@ -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,141 @@
|
|||
import type { Ref } from 'vue';
|
||||
import { ref, onBeforeUnmount, onMounted } from 'vue';
|
||||
import useState from '../../../_util/hooks/useState';
|
||||
|
||||
type TouchEventHandler = (e: TouchEvent) => void;
|
||||
type WheelEventHandler = (e: WheelEvent) => void;
|
||||
|
||||
const MIN_SWIPE_DISTANCE = 0.1;
|
||||
const STOP_SWIPE_DISTANCE = 0.01;
|
||||
const REFRESH_INTERVAL = 20;
|
||||
const SPEED_OFF_MULTIPLE = 0.995 ** REFRESH_INTERVAL;
|
||||
|
||||
// ================================= Hook =================================
|
||||
export default function useTouchMove(
|
||||
domRef: Ref<HTMLDivElement>,
|
||||
onOffset: (offsetX: number, offsetY: number) => boolean,
|
||||
) {
|
||||
const [touchPosition, setTouchPosition] = useState<{ x: number; y: number }>();
|
||||
const [lastTimestamp, setLastTimestamp] = useState<number>(0);
|
||||
const [lastTimeDiff, setLastTimeDiff] = useState<number>(0);
|
||||
const [lastOffset, setLastOffset] = useState<{ x: number; y: number }>();
|
||||
const motionRef = ref<number>();
|
||||
|
||||
// ========================= Events =========================
|
||||
// >>> Touch events
|
||||
function onTouchStart(e: TouchEvent) {
|
||||
const { screenX, screenY } = e.touches[0];
|
||||
setTouchPosition({ x: screenX, y: screenY });
|
||||
window.clearInterval(motionRef.value);
|
||||
}
|
||||
|
||||
function onTouchMove(e: TouchEvent) {
|
||||
if (!touchPosition.value) return;
|
||||
|
||||
e.preventDefault();
|
||||
const { screenX, screenY } = e.touches[0];
|
||||
setTouchPosition({ x: screenX, y: screenY });
|
||||
const offsetX = screenX - touchPosition.value.x;
|
||||
const offsetY = screenY - touchPosition.value.y;
|
||||
onOffset(offsetX, offsetY);
|
||||
const now = Date.now();
|
||||
setLastTimestamp(now);
|
||||
setLastTimeDiff(now - lastTimestamp.value);
|
||||
setLastOffset({ x: offsetX, y: offsetY });
|
||||
}
|
||||
|
||||
function onTouchEnd() {
|
||||
if (!touchPosition.value) return;
|
||||
|
||||
setTouchPosition(null);
|
||||
setLastOffset(null);
|
||||
|
||||
// Swipe if needed
|
||||
if (lastOffset.value) {
|
||||
const distanceX = lastOffset.value.x / lastTimeDiff.value;
|
||||
const distanceY = lastOffset.value.y / lastTimeDiff.value;
|
||||
const absX = Math.abs(distanceX);
|
||||
const absY = Math.abs(distanceY);
|
||||
|
||||
// Skip swipe if low distance
|
||||
if (Math.max(absX, absY) < MIN_SWIPE_DISTANCE) return;
|
||||
|
||||
let currentX = distanceX;
|
||||
let currentY = distanceY;
|
||||
|
||||
motionRef.value = window.setInterval(() => {
|
||||
if (Math.abs(currentX) < STOP_SWIPE_DISTANCE && Math.abs(currentY) < STOP_SWIPE_DISTANCE) {
|
||||
window.clearInterval(motionRef.value);
|
||||
return;
|
||||
}
|
||||
|
||||
currentX *= SPEED_OFF_MULTIPLE;
|
||||
currentY *= SPEED_OFF_MULTIPLE;
|
||||
onOffset(currentX * REFRESH_INTERVAL, currentY * REFRESH_INTERVAL);
|
||||
}, REFRESH_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
// >>> Wheel event
|
||||
const lastWheelDirectionRef = ref<'x' | 'y'>();
|
||||
|
||||
function onWheel(e: WheelEvent) {
|
||||
const { deltaX, deltaY } = e;
|
||||
|
||||
// Convert both to x & y since wheel only happened on PC
|
||||
let mixed = 0;
|
||||
const absX = Math.abs(deltaX);
|
||||
const absY = Math.abs(deltaY);
|
||||
if (absX === absY) {
|
||||
mixed = lastWheelDirectionRef.value === 'x' ? deltaX : deltaY;
|
||||
} else if (absX > absY) {
|
||||
mixed = deltaX;
|
||||
lastWheelDirectionRef.value = 'x';
|
||||
} else {
|
||||
mixed = deltaY;
|
||||
lastWheelDirectionRef.value = 'y';
|
||||
}
|
||||
|
||||
if (onOffset(-mixed, -mixed)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================= Effect =========================
|
||||
const touchEventsRef = ref<{
|
||||
onTouchStart: TouchEventHandler;
|
||||
onTouchMove: TouchEventHandler;
|
||||
onTouchEnd: TouchEventHandler;
|
||||
onWheel: WheelEventHandler;
|
||||
}>({
|
||||
onTouchStart,
|
||||
onTouchMove,
|
||||
onTouchEnd,
|
||||
onWheel,
|
||||
});
|
||||
function onProxyTouchStart(e: TouchEvent) {
|
||||
touchEventsRef.value.onTouchStart(e);
|
||||
}
|
||||
function onProxyTouchMove(e: TouchEvent) {
|
||||
touchEventsRef.value.onTouchMove(e);
|
||||
}
|
||||
function onProxyTouchEnd(e: TouchEvent) {
|
||||
touchEventsRef.value.onTouchEnd(e);
|
||||
}
|
||||
function onProxyWheel(e: WheelEvent) {
|
||||
touchEventsRef.value.onWheel(e);
|
||||
}
|
||||
onMounted(() => {
|
||||
document.addEventListener('touchmove', onProxyTouchMove, { passive: false });
|
||||
document.addEventListener('touchend', onProxyTouchEnd, { passive: false });
|
||||
|
||||
// No need to clean up since element removed
|
||||
domRef.value.addEventListener('touchstart', onProxyTouchStart, { passive: false });
|
||||
domRef.value.addEventListener('wheel', onProxyWheel);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('touchmove', onProxyTouchMove);
|
||||
document.removeEventListener('touchend', onProxyTouchEnd);
|
||||
});
|
||||
}
|
|
@ -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,51 @@
|
|||
import type { Key, VueNode } from '../../_util/type';
|
||||
import type { TabPaneProps } from './TabPanelList/TabPane';
|
||||
|
||||
export type TabSizeMap = Map<Key, { width: number; height: number; left: number; top: number }>;
|
||||
|
||||
export interface TabOffset {
|
||||
width: number;
|
||||
height: number;
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
}
|
||||
export type TabOffsetMap = Map<Key, TabOffset>;
|
||||
|
||||
export type TabPosition = 'left' | 'right' | 'top' | 'bottom';
|
||||
|
||||
export interface Tab extends TabPaneProps {
|
||||
key: string;
|
||||
node: VueNode;
|
||||
}
|
||||
|
||||
export type RenderTabBar = (props: any, DefaultTabBar: any) => VueNode;
|
||||
|
||||
export interface TabsLocale {
|
||||
dropdownAriaLabel?: string;
|
||||
removeAriaLabel?: string;
|
||||
addAriaLabel?: string;
|
||||
}
|
||||
|
||||
export interface EditableConfig {
|
||||
onEdit: (
|
||||
type: 'add' | 'remove',
|
||||
info: { key?: string; event: MouseEvent | KeyboardEvent },
|
||||
) => void;
|
||||
showAdd?: boolean;
|
||||
removeIcon?: () => VueNode;
|
||||
addIcon?: () => VueNode;
|
||||
}
|
||||
|
||||
export interface AnimatedConfig {
|
||||
inkBar?: boolean;
|
||||
tabPane?: boolean;
|
||||
}
|
||||
|
||||
export type OnTabScroll = (info: { direction: 'left' | 'right' | 'top' | 'bottom' }) => void;
|
||||
|
||||
export type TabBarExtraPosition = 'left' | 'right';
|
||||
|
||||
export type TabBarExtraMap = Partial<Record<TabBarExtraPosition, any>>;
|
||||
|
||||
export type TabBarExtraContent = VueNode;
|
|
@ -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();
|
||||
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
// ========================== Navigation ==========================
|
||||
> .@{tab-prefix-cls}-nav,
|
||||
> div > .@{tab-prefix-cls}-nav {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.clearfix();
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
|
||||
&-ink-bar {
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background-color: @tabs-ink-bar-color;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
&-bar {
|
||||
margin: @tabs-bar-margin;
|
||||
border-bottom: @border-width-base @border-style-base @border-color-split;
|
||||
outline: none;
|
||||
transition: padding 0.3s @ease-in-out;
|
||||
}
|
||||
|
||||
&-nav-container {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: -1px;
|
||||
overflow: hidden;
|
||||
font-size: @tabs-title-font-size;
|
||||
line-height: @line-height-base;
|
||||
white-space: nowrap;
|
||||
transition: padding 0.3s @ease-in-out;
|
||||
.clearfix();
|
||||
|
||||
&-scrolling {
|
||||
padding-right: @tabs-scrolling-size;
|
||||
padding-left: @tabs-scrolling-size;
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/9104
|
||||
&-bottom &-bottom-bar {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 0;
|
||||
border-top: @border-width-base @border-style-base @border-color-split;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&-bottom &-bottom-bar &-ink-bar {
|
||||
top: 1px;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
&-bottom &-bottom-bar &-nav-container {
|
||||
margin-top: -1px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&-tab-prev,
|
||||
&-tab-next {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
color: @text-color-secondary;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: width 0.3s @ease-in-out, opacity 0.3s @ease-in-out, color 0.3s @ease-in-out;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
|
||||
&.@{tab-prefix-cls}-tab-arrow-show {
|
||||
width: @tabs-scrolling-size;
|
||||
height: 100%;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
line-height: inherit;
|
||||
text-align: center;
|
||||
text-transform: none;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
&-target {
|
||||
display: block;
|
||||
.iconfont-size-under-12px(10px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-tab-btn-disabled {
|
||||
cursor: not-allowed;
|
||||
&,
|
||||
&:hover {
|
||||
color: @disabled-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-tab-next {
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
&-tab-prev {
|
||||
left: 0;
|
||||
:root & {
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-nav-wrap {
|
||||
margin-bottom: -1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-nav-scroll {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-nav {
|
||||
.@{tab-prefix-cls}-nav-wrap {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
transition: transform 0.3s @ease-in-out;
|
||||
display: flex;
|
||||
flex: auto;
|
||||
align-self: stretch;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
transform: translate(0); // Fix chrome render bug
|
||||
|
||||
// >>>>> Ping shadow
|
||||
&::before,
|
||||
&::after {
|
||||
display: table;
|
||||
content: ' ';
|
||||
}
|
||||
|
||||
&::after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-tab {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
margin: @tabs-horizontal-margin;
|
||||
padding: @tabs-horizontal-padding;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s @ease-in-out;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
border-top: 2px solid transparent;
|
||||
border-radius: @border-radius-base @border-radius-base 0 0;
|
||||
transition: all 0.3s;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
transition: opacity @animation-duration-slow;
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
.@{tab-prefix-cls}-nav-list {
|
||||
position: relative;
|
||||
display: flex;
|
||||
transition: transform @animation-duration-slow;
|
||||
}
|
||||
|
||||
// >>>>>>>> Operations
|
||||
.@{tab-prefix-cls}-nav-operations {
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
|
||||
&-hidden {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-nav-more {
|
||||
position: relative;
|
||||
padding: @tabs-card-horizontal-padding;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 5px;
|
||||
transform: translateY(100%);
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-nav-add {
|
||||
min-width: @tabs-card-height;
|
||||
padding: 0 @padding-xs;
|
||||
background: @tabs-card-head-background;
|
||||
border: @border-width-base @border-style-base @border-color-split;
|
||||
border-radius: @border-radius-base @border-radius-base 0 0;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: all @animation-duration-slow @ease-in-out;
|
||||
|
||||
&:hover {
|
||||
color: @tabs-hover-color;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
color: @tabs-active-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-extra-content {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
&-centered {
|
||||
> .@{tab-prefix-cls}-nav,
|
||||
> div > .@{tab-prefix-cls}-nav {
|
||||
.@{tab-prefix-cls}-nav-wrap {
|
||||
&:not([class*='@{tab-prefix-cls}-nav-wrap-ping']) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ InkBar ============================
|
||||
&-ink-bar {
|
||||
position: absolute;
|
||||
background: @tabs-ink-bar-color;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// ============================= Tabs =============================
|
||||
&-tab {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: @tabs-horizontal-padding;
|
||||
font-size: @tabs-title-font-size;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
|
||||
&-btn,
|
||||
&-remove {
|
||||
&:focus,
|
||||
&:active {
|
||||
color: @tabs-active-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-btn {
|
||||
outline: none;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
&-remove {
|
||||
flex: none;
|
||||
margin-right: -@margin-xss;
|
||||
margin-left: @margin-xs;
|
||||
color: @text-color-secondary;
|
||||
font-size: @font-size-sm;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: all @animation-duration-slow;
|
||||
|
||||
&:hover {
|
||||
color: @heading-color;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: @tabs-hover-color;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: @tabs-active-color;
|
||||
}
|
||||
|
||||
.@{iconfont-css-prefix} {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&-active {
|
||||
&&-active &-btn {
|
||||
color: @tabs-highlight-color;
|
||||
// https://github.com/vueComponent/ant-design-vue/issues/4241
|
||||
// Remove font-weight to keep pace with antd (#4241)
|
||||
text-shadow: 0 0 0.25px currentColor;
|
||||
// font-weight: 500;
|
||||
}
|
||||
|
||||
&-disabled {
|
||||
&,
|
||||
&:hover {
|
||||
&&-disabled {
|
||||
color: @disabled-color;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&&-disabled &-btn,
|
||||
&&-disabled &-remove {
|
||||
&:focus,
|
||||
&:active {
|
||||
color: @disabled-color;
|
||||
}
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-large-bar {
|
||||
.@{tab-prefix-cls}-nav-container {
|
||||
font-size: @tabs-title-font-size-lg;
|
||||
& &-remove .@{iconfont-css-prefix} {
|
||||
margin: 0;
|
||||
}
|
||||
.@{tab-prefix-cls}-tab {
|
||||
padding: @tabs-horizontal-padding-lg;
|
||||
|
||||
.@{iconfont-css-prefix} {
|
||||
margin-right: @margin-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-small-bar {
|
||||
.@{tab-prefix-cls}-nav-container {
|
||||
font-size: @tabs-title-font-size-sm;
|
||||
}
|
||||
.@{tab-prefix-cls}-tab {
|
||||
padding: @tabs-horizontal-padding-sm;
|
||||
}
|
||||
&-tab + &-tab {
|
||||
margin: @tabs-horizontal-margin;
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-centered-bar {
|
||||
.@{tab-prefix-cls}-nav-wrap {
|
||||
text-align: center;
|
||||
}
|
||||
// =========================== TabPanes ===========================
|
||||
&-content {
|
||||
&-holder {
|
||||
flex: auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
// Create an empty element to avoid margin collapsing
|
||||
// https://github.com/ant-design/ant-design/issues/18103
|
||||
&-content::before {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
content: '';
|
||||
}
|
||||
|
||||
// Horizontal Content
|
||||
.@{tab-prefix-cls}-top-content,
|
||||
.@{tab-prefix-cls}-bottom-content {
|
||||
width: 100%;
|
||||
|
||||
> .@{tab-prefix-cls}-tabpane {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
-webkit-backface-visibility: hidden;
|
||||
opacity: 1;
|
||||
transition: opacity 0.45s;
|
||||
}
|
||||
|
||||
> .@{tab-prefix-cls}-tabpane-inactive {
|
||||
.tabs-hidden-content();
|
||||
}
|
||||
|
||||
&.@{tab-prefix-cls}-content-animated {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
transition: margin-left 0.3s @ease-in-out;
|
||||
will-change: margin-left;
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical Bar
|
||||
.@{tab-prefix-cls}-left-bar,
|
||||
.@{tab-prefix-cls}-right-bar {
|
||||
height: 100%;
|
||||
border-bottom: 0;
|
||||
|
||||
.@{tab-prefix-cls}-tab-arrow-show {
|
||||
width: 100%;
|
||||
height: @tabs-scrolling-size;
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-tab {
|
||||
display: block;
|
||||
float: none;
|
||||
margin: @tabs-vertical-margin;
|
||||
padding: @tabs-vertical-padding;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
&-animated {
|
||||
transition: margin @animation-duration-slow;
|
||||
}
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-extra-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-nav-scroll {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-nav-container,
|
||||
.@{tab-prefix-cls}-nav-wrap {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-nav-container {
|
||||
margin-bottom: 0;
|
||||
|
||||
&.@{tab-prefix-cls}-nav-container-scrolling {
|
||||
padding: @tabs-scrolling-size 0;
|
||||
}
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-nav-wrap {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-nav {
|
||||
&-tabpane {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-ink-bar {
|
||||
top: 0;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
width: 2px;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-tab-next {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: @tabs-scrolling-size;
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-tab-prev {
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: @tabs-scrolling-size;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical Content
|
||||
.@{tab-prefix-cls}-left-content,
|
||||
.@{tab-prefix-cls}-right-content {
|
||||
width: auto;
|
||||
margin-top: 0 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Vertical - Left
|
||||
.@{tab-prefix-cls}-left-bar {
|
||||
float: left;
|
||||
margin-right: -1px;
|
||||
margin-bottom: 0;
|
||||
border-right: @border-width-base @border-style-base @border-color-split;
|
||||
.@{tab-prefix-cls}-tab {
|
||||
text-align: right;
|
||||
}
|
||||
.@{tab-prefix-cls}-nav-container {
|
||||
margin-right: -1px;
|
||||
}
|
||||
.@{tab-prefix-cls}-nav-wrap {
|
||||
margin-right: -1px;
|
||||
}
|
||||
.@{tab-prefix-cls}-ink-bar {
|
||||
right: 1px;
|
||||
}
|
||||
}
|
||||
.@{tab-prefix-cls}-left-content {
|
||||
padding-left: 24px;
|
||||
border-left: @border-width-base @border-style-base @border-color-split;
|
||||
}
|
||||
|
||||
// Vertical - Right
|
||||
.@{tab-prefix-cls}-right-bar {
|
||||
float: right;
|
||||
margin-bottom: 0;
|
||||
margin-left: -1px;
|
||||
border-left: @border-width-base @border-style-base @border-color-split;
|
||||
.@{tab-prefix-cls}-nav-container {
|
||||
margin-left: -1px;
|
||||
}
|
||||
.@{tab-prefix-cls}-nav-wrap {
|
||||
margin-left: -1px;
|
||||
}
|
||||
.@{tab-prefix-cls}-ink-bar {
|
||||
left: 1px;
|
||||
}
|
||||
}
|
||||
.@{tab-prefix-cls}-right-content {
|
||||
padding-right: 24px;
|
||||
border-right: @border-width-base @border-style-base @border-color-split;
|
||||
}
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-top .@{tab-prefix-cls}-ink-bar-animated,
|
||||
.@{tab-prefix-cls}-bottom .@{tab-prefix-cls}-ink-bar-animated {
|
||||
transition: transform 0.3s @ease-in-out, width 0.2s @ease-in-out, left 0.3s @ease-in-out;
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-left .@{tab-prefix-cls}-ink-bar-animated,
|
||||
.@{tab-prefix-cls}-right .@{tab-prefix-cls}-ink-bar-animated {
|
||||
transition: transform 0.3s @ease-in-out, height 0.2s @ease-in-out, top 0.3s @ease-in-out;
|
||||
}
|
||||
|
||||
// No animation
|
||||
.tabs-no-animation() {
|
||||
> .@{tab-prefix-cls}-content-animated {
|
||||
margin-left: 0 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
> .@{tab-prefix-cls}-tabpane-inactive {
|
||||
.tabs-hidden-content();
|
||||
}
|
||||
}
|
||||
|
||||
.no-flex,
|
||||
.@{tab-prefix-cls}-no-animation {
|
||||
> .@{tab-prefix-cls}-content {
|
||||
.tabs-no-animation();
|
||||
}
|
||||
}
|
||||
|
||||
.@{tab-prefix-cls}-left-content,
|
||||
.@{tab-prefix-cls}-right-content {
|
||||
.tabs-no-animation();
|
||||
}
|
||||
|
|
|
@ -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,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,5 +1,5 @@
|
|||
// debugger tsx
|
||||
import Demo from '../../components/auto-complete/demo/index.vue';
|
||||
import Demo from '../../components/tabs/demo/basic.vue';
|
||||
|
||||
export default {
|
||||
render() {
|
||||
|
|
Loading…
Reference in New Issue