refactor: tabs

refactor-tabs
tangjinzhou 2021-10-06 10:18:00 +08:00
parent 53625976b8
commit cd9f592c2e
10 changed files with 137 additions and 126 deletions

View File

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

View File

@ -1,10 +1,10 @@
import type { Tab } from './interface';
import type { PropType, InjectionKey } from 'vue';
import { provide, inject, defineComponent } from 'vue';
import type { PropType, InjectionKey, Ref } from 'vue';
import { provide, inject, defineComponent, toRefs, ref } from 'vue';
export interface TabContextProps {
tabs: Tab[];
prefixCls: string;
tabs: Ref<Tab[]>;
prefixCls: Ref<string>;
}
const TabsContextKey: InjectionKey<TabContextProps> = Symbol('tabsContextKey');
@ -14,7 +14,7 @@ export const useProvideTabs = (props: TabContextProps) => {
};
export const useInjectTabs = () => {
return inject(TabsContextKey, { tabs: [], prefixCls: undefined });
return inject(TabsContextKey, { tabs: ref([]), prefixCls: ref() });
};
const TabsContextProvider = defineComponent({
@ -25,7 +25,7 @@ const TabsContextProvider = defineComponent({
prefixCls: { type: String, default: undefined },
},
setup(props, { slots }) {
useProvideTabs(props);
useProvideTabs(toRefs(props));
return () => slots.default?.();
},
});

View File

@ -16,7 +16,7 @@ export interface OperationNodeProps {
tabs: Tab[];
rtl: boolean;
tabBarGutter?: number;
activeKey: string;
activeKey: Key;
mobile: boolean;
moreIcon?: VueNode;
moreTransitionName?: string;
@ -34,7 +34,7 @@ export default defineComponent({
tabs: { type: Object as PropType<Tab[]> },
rtl: { type: Boolean },
tabBarGutter: { type: Number },
activeKey: { type: String },
activeKey: { type: [String, Number] },
mobile: { type: Boolean },
moreIcon: PropTypes.any,
moreTransitionName: { type: String },
@ -47,7 +47,7 @@ export default defineComponent({
setup(props, { attrs, slots }) {
// ======================== Dropdown ========================
const [open, setOpen] = useState(false);
const [selectedKey, setSelectedKey] = useState<string>(null);
const [selectedKey, setSelectedKey] = useState<Key>(null);
const selectOffset = (offset: -1 | 1) => {
const enabledTabs = props.tabs.filter(tab => !tab.disabled);
let selectedIndex = enabledTabs.findIndex(tab => tab.key === selectedKey.value) || 0;

View File

@ -32,7 +32,7 @@ const tabNavListProps = () => {
return {
id: { type: String },
tabPosition: { type: String as PropType<TabPosition> },
activeKey: { type: String },
activeKey: { type: [String, Number] },
rtl: { type: Boolean },
panes: PropTypes.any,
animated: { type: Object as PropType<AnimatedConfig>, default: undefined as AnimatedConfig },
@ -66,7 +66,7 @@ export default defineComponent({
slots: ['panes', 'moreIcon', 'extra'],
emits: ['tabClick', 'tabScroll'],
setup(props, { attrs, slots }) {
const tabsContext = useInjectTabs();
const { tabs, prefixCls } = useInjectTabs();
const tabsWrapperRef = ref<HTMLDivElement>();
const tabListRef = ref<HTMLDivElement>();
const operationsRef = ref<{ $el: HTMLDivElement }>();
@ -98,15 +98,10 @@ export default defineComponent({
const [addHeight, setAddHeight] = useState<number>(0);
const [tabSizes, setTabSizes] = useRafState<TabSizeMap>(new Map());
const tabOffsets = useOffsets(
computed(() => tabsContext.tabs),
tabSizes,
);
const tabOffsets = useOffsets(tabs, tabSizes);
// ========================== Util =========================
const operationsHiddenClassName = computed(
() => `${tabsContext.prefixCls}-nav-operations-hidden`,
);
const operationsHiddenClassName = computed(() => `${prefixCls.value}-nav-operations-hidden`);
const transformMin = ref(0);
const transformMax = ref(0);
@ -261,15 +256,15 @@ export default defineComponent({
mergedBasicSize = basicSize - addSize;
}
const { tabs } = tabsContext;
if (!tabs.length) {
const tabsVal = tabs.value;
if (!tabsVal.length) {
[visibleStart.value, visibleEnd.value] = [0, 0];
}
const len = tabs.length;
const len = tabsVal.length;
let endIndex = len;
for (let i = 0; i < len; i += 1) {
const offset = tabOffsets.value.get(tabs[i].key) || DEFAULT_SIZE;
const offset = tabOffsets.value.get(tabsVal[i].key) || DEFAULT_SIZE;
if (offset[position] + offset[unit] > transformSize + mergedBasicSize) {
endIndex = i - 1;
break;
@ -278,7 +273,7 @@ export default defineComponent({
let startIndex = 0;
for (let i = len - 1; i >= 0; i -= 1) {
const offset = tabOffsets.value.get(tabs[i].key) || DEFAULT_SIZE;
const offset = tabOffsets.value.get(tabsVal[i].key) || DEFAULT_SIZE;
if (offset[position] < transformSize) {
startIndex = i + 1;
break;
@ -319,7 +314,7 @@ export default defineComponent({
// Update buttons records
setTabSizes(() => {
const newSizes: TabSizeMap = new Map();
tabsContext.tabs.forEach(({ key }) => {
tabs.value.forEach(({ key }) => {
const btnRef = getBtnRef(key).value;
const btnNode = (btnRef as any).$el || btnRef;
if (btnNode) {
@ -337,8 +332,8 @@ export default defineComponent({
// ======================== Dropdown =======================
const hiddenTabs = computed(() => [
...tabsContext.tabs.slice(0, visibleStart.value),
...tabsContext.tabs.slice(visibleEnd.value + 1),
...tabs.value.slice(0, visibleStart.value),
...tabs.value.slice(visibleEnd.value + 1),
]);
// =================== Link & Operations ===================
@ -385,7 +380,7 @@ export default defineComponent({
);
watch(
[() => props.rtl, () => props.tabBarGutter, () => props.activeKey, () => tabsContext.tabs],
[() => props.rtl, () => props.tabBarGutter, () => props.activeKey, () => tabs.value],
() => {
onListHolderResize();
},
@ -406,7 +401,6 @@ export default defineComponent({
});
return () => {
const { prefixCls, tabs } = tabsContext;
const {
id,
animated,
@ -420,9 +414,10 @@ export default defineComponent({
onTabClick,
} = props;
const { class: className, style } = attrs;
const pre = prefixCls.value;
// ========================= Render ========================
const hasDropdown = !!hiddenTabs.value.length;
const wrapPrefix = `${prefixCls}-nav-wrap`;
const wrapPrefix = `${pre}-nav-wrap`;
let pingLeft: boolean;
let pingRight: boolean;
let pingTop: boolean;
@ -450,12 +445,12 @@ export default defineComponent({
typeof tabBarGutter === 'number' ? `${tabBarGutter}px` : tabBarGutter;
}
const tabNodes = tabs.map((tab, i) => {
const tabNodes = tabs.value.map((tab, i) => {
const { key } = tab;
return (
<TabNode
id={id}
prefixCls={prefixCls}
prefixCls={pre}
key={key}
tab={tab}
/* first node should not have margin left */
@ -492,14 +487,14 @@ export default defineComponent({
<div
ref={ref}
role="tablist"
class={classNames(`${prefixCls}-nav`, className)}
class={classNames(`${pre}-nav`, className)}
style={style}
onKeydown={() => {
// No need animation when use keyboard
doLockAnimation();
}}
>
<ExtraContent position="left" extra={extra} prefixCls={prefixCls} />
<ExtraContent position="left" extra={extra} prefixCls={pre} />
<ResizeObserver onResize={onListHolderResize}>
<div
@ -514,7 +509,7 @@ export default defineComponent({
<ResizeObserver onResize={onListHolderResize}>
<div
ref={tabListRef}
class={`${prefixCls}-nav-list`}
class={`${pre}-nav-list`}
style={{
transform: `translate(${transformLeft.value}px, ${transformTop.value}px)`,
transition: lockAnimation.value ? 'none' : undefined,
@ -523,7 +518,7 @@ export default defineComponent({
{tabNodes}
<AddButton
ref={innerAddButtonRef}
prefixCls={prefixCls}
prefixCls={pre}
locale={locale}
editable={editable}
style={{
@ -533,8 +528,8 @@ export default defineComponent({
/>
<div
class={classNames(`${prefixCls}-ink-bar`, {
[`${prefixCls}-ink-bar-animated`]: animated.inkBar,
class={classNames(`${pre}-ink-bar`, {
[`${pre}-ink-bar-animated`]: animated.inkBar,
})}
style={inkStyle.value}
/>
@ -546,12 +541,12 @@ export default defineComponent({
<OperationNode
{...props}
ref={operationsRef}
prefixCls={prefixCls}
prefixCls={pre}
tabs={hiddenTabs.value}
class={!hasDropdown && operationsHiddenClassName.value}
/>
<ExtraContent position="right" extra={extra} prefixCls={prefixCls} />
<ExtraContent position="right" extra={extra} prefixCls={pre} />
</div>
);
};

View File

@ -1,6 +1,6 @@
import { defineComponent, ref, watch, computed } from 'vue';
import type { CSSProperties } from 'vue';
import type { VueNode } from '../../../_util/type';
import type { VueNode, Key } from '../../../_util/type';
import PropTypes from '../../../_util/vue-types';
export interface TabPaneProps {
@ -12,7 +12,7 @@ export interface TabPaneProps {
// Pass by TabPaneList
prefixCls?: string;
tabKey?: string;
tabKey?: Key;
id?: string;
animated?: boolean;
active?: boolean;
@ -33,7 +33,7 @@ export default defineComponent({
// Pass by TabPaneList
prefixCls: { type: String },
tabKey: { type: String },
tabKey: { type: [String, Number] },
id: { type: String },
},
slots: ['closeIcon', 'tab'],

View File

@ -25,22 +25,20 @@ export default defineComponent({
destroyInactiveTabPane: { type: Boolean },
},
setup(props) {
const tabsContext = useInjectTabs();
const { tabs, prefixCls } = 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);
const pre = prefixCls.value;
const activeIndex = tabs.value.findIndex(tab => tab.key === activeKey);
return (
<div class={`${prefixCls}-content-holder`}>
<div class={`${pre}-content-holder`}>
<div
class={[
`${prefixCls}-content`,
`${prefixCls}-content-${tabPosition}`,
`${pre}-content`,
`${pre}-content-${tabPosition}`,
{
[`${prefixCls}-content-animated`]: tabPaneAnimated,
[`${pre}-content-animated`]: tabPaneAnimated,
},
]}
style={
@ -49,10 +47,10 @@ export default defineComponent({
: null
}
>
{tabs.map(tab => {
{tabs.value.map(tab => {
return cloneElement(tab.node, {
key: tab.key,
prefixCls,
prefixCls: pre,
tabKey: tab.key,
id,
animated: tabPaneAnimated,

View File

@ -21,7 +21,8 @@ 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';
import { useProvideTabs } from './TabContext';
import type { Key } from '../../_util/type';
export type TabsType = 'line' | 'card' | 'editable-card';
export type TabsPosition = 'top' | 'right' | 'bottom' | 'left';
@ -34,8 +35,8 @@ export const tabsProps = () => {
prefixCls: { type: String },
id: { type: String },
activeKey: { type: String },
defaultActiveKey: { type: String },
activeKey: { type: [String, Number], required: true },
defaultActiveKey: { type: [String, Number] },
direction: { type: String as PropType<'ltr' | 'rtl'> },
animated: { type: [Boolean, Object] as PropType<boolean | AnimatedConfig> },
renderTabBar: { type: Function as PropType<RenderTabBar> },
@ -50,12 +51,12 @@ export const tabsProps = () => {
centered: Boolean,
onEdit: {
type: Function as PropType<
(e: MouseEvent | KeyboardEvent | string, action: 'add' | 'remove') => void
(e: MouseEvent | KeyboardEvent | Key, action: 'add' | 'remove') => void
>,
},
onChange: { type: Function as PropType<(activeKey: string) => void> },
onChange: { type: Function as PropType<(activeKey: Key) => void> },
onTabClick: {
type: Function as PropType<(activeKey: string, e: KeyboardEvent | MouseEvent) => void>,
type: Function as PropType<(activeKey: Key, e: KeyboardEvent | MouseEvent) => void>,
},
onTabScroll: { type: Function as PropType<OnTabScroll> },
@ -78,7 +79,7 @@ function parseTabList(children: any[]): Tab[] {
props[camelize(k)] = v;
}
const slots = node.children || {};
const key = node.key !== undefined ? String(node.key) : undefined;
const key = node.key !== undefined ? node.key : undefined;
const {
tab = slots.tab,
disabled,
@ -159,7 +160,7 @@ const InternalTabs = defineComponent({
});
// ====================== Active Key ======================
const [mergedActiveKey, setMergedActiveKey] = useMergedState<string>(() => props.tabs[0]?.key, {
const [mergedActiveKey, setMergedActiveKey] = useMergedState<Key>(() => props.tabs[0]?.key, {
value: computed(() => props.activeKey),
defaultValue: props.defaultActiveKey,
});
@ -171,7 +172,7 @@ const InternalTabs = defineComponent({
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);
mergedActiveKey.value = props.tabs[newActiveIndex]?.key;
}
setActiveIndex(newActiveIndex);
});
@ -197,30 +198,30 @@ const InternalTabs = defineComponent({
});
// ======================== Events ========================
const onInternalTabClick = (key: string, e: MouseEvent | KeyboardEvent) => {
const onInternalTabClick = (key: Key, e: MouseEvent | KeyboardEvent) => {
props.onTabClick?.(key, e);
setMergedActiveKey(key);
props.onChange?.(key);
};
useProvideTabs({
tabs: computed(() => props.tabs),
prefixCls,
});
return () => {
const {
id,
type,
activeKey,
defaultActiveKey,
tabBarGutter,
tabBarStyle,
locale,
destroyInactiveTabPane,
renderTabBar,
onChange,
onTabClick,
onTabScroll,
hideAdd,
centered,
...restProps
} = props;
// ======================== Render ========================
const sharedProps = {
@ -274,34 +275,31 @@ const InternalTabs = defineComponent({
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>
<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,
)}
>
{tabNavBar}
<TabPanelList
destroyInactiveTabPane={destroyInactiveTabPane}
{...sharedProps}
animated={mergedAnimated.value}
/>
</div>
);
};
},

View File

@ -1,3 +1,4 @@
import supportsPassive from 'ant-design-vue/es/_util/supportsPassive';
import type { Ref } from 'vue';
import { ref, onBeforeUnmount, onMounted } from 'vue';
import useState from '../../../_util/hooks/useState';
@ -9,7 +10,6 @@ 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>,
@ -131,7 +131,11 @@ export default function useTouchMove(
// No need to clean up since element removed
domRef.value.addEventListener('touchstart', onProxyTouchStart, { passive: false });
domRef.value.addEventListener('wheel', onProxyWheel);
domRef.value.addEventListener(
'wheel',
onProxyWheel,
supportsPassive ? { passive: true } : false,
);
});
onBeforeUnmount(() => {

View File

@ -15,7 +15,7 @@ export type TabOffsetMap = Map<Key, TabOffset>;
export type TabPosition = 'left' | 'right' | 'top' | 'bottom';
export interface Tab extends TabPaneProps {
key: string;
key: Key;
node: VueNode;
}
@ -28,10 +28,7 @@ export interface TabsLocale {
}
export interface EditableConfig {
onEdit: (
type: 'add' | 'remove',
info: { key?: string; event: MouseEvent | KeyboardEvent },
) => void;
onEdit: (type: 'add' | 'remove', info: { key?: Key; event: MouseEvent | KeyboardEvent }) => void;
showAdd?: boolean;
removeIcon?: () => VueNode;
addIcon?: () => VueNode;

View File

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