ant-design-vue/components/tabs/src/TabNavList/OperationNode.tsx

235 lines
7.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import Menu, { MenuItem } from '../../../menu';
import Dropdown from '../../../vc-dropdown';
import type { Tab, TabsLocale, EditableConfig } from '../interface';
import AddButton from './AddButton';
import type { Key } from '../../../_util/type';
import KeyCode from '../../../_util/KeyCode';
import type { CSSProperties, ExtractPropTypes, PropType } from 'vue';
import classNames from '../../../_util/classNames';
import { defineComponent, watch, computed, onMounted } from 'vue';
import PropTypes from '../../../_util/vue-types';
import useState from '../../../_util/hooks/useState';
import { EllipsisOutlined } from '@ant-design/icons-vue';
const operationNodeProps = {
prefixCls: { type: String },
id: { type: String },
tabs: { type: Object as PropType<Tab[]> },
rtl: { type: Boolean },
tabBarGutter: { type: Number },
activeKey: { type: [String, Number] },
mobile: { type: Boolean },
moreIcon: PropTypes.any,
moreTransitionName: { type: String },
editable: { type: Object as PropType<EditableConfig> },
locale: { type: Object as PropType<TabsLocale>, default: undefined as TabsLocale },
removeAriaLabel: String,
onTabClick: { type: Function as PropType<(key: Key, e: MouseEvent | KeyboardEvent) => void> },
};
export type OperationNodeProps = Partial<ExtractPropTypes<typeof operationNodeProps>>;
export default defineComponent({
name: 'OperationNode',
inheritAttrs: false,
props: operationNodeProps,
emits: ['tabClick'],
slots: ['moreIcon'],
setup(props, { attrs, slots }) {
// ======================== Dropdown ========================
const [open, setOpen] = useState(false);
const [selectedKey, setSelectedKey] = useState<Key>(null);
const selectOffset = (offset: -1 | 1) => {
const enabledTabs = props.tabs.filter(tab => !tab.disabled);
let selectedIndex = enabledTabs.findIndex(tab => tab.key === selectedKey.value) || 0;
const len = enabledTabs.length;
for (let i = 0; i < len; i += 1) {
selectedIndex = (selectedIndex + offset + len) % len;
const tab = enabledTabs[selectedIndex];
if (!tab.disabled) {
setSelectedKey(tab.key);
return;
}
}
};
const onKeyDown = (e: KeyboardEvent) => {
const { which } = e;
if (!open.value) {
if ([KeyCode.DOWN, KeyCode.SPACE, KeyCode.ENTER].includes(which)) {
setOpen(true);
e.preventDefault();
}
return;
}
switch (which) {
case KeyCode.UP:
selectOffset(-1);
e.preventDefault();
break;
case KeyCode.DOWN:
selectOffset(1);
e.preventDefault();
break;
case KeyCode.ESC:
setOpen(false);
break;
case KeyCode.SPACE:
case KeyCode.ENTER:
if (selectedKey.value !== null) props.onTabClick(selectedKey.value, e);
break;
}
};
const popupId = computed(() => `${props.id}-more-popup`);
const selectedItemId = computed(() =>
selectedKey.value !== null ? `${popupId.value}-${selectedKey.value}` : null,
);
const onRemoveTab = (event: MouseEvent | KeyboardEvent, key: Key) => {
event.preventDefault();
event.stopPropagation();
props.editable.onEdit('remove', {
key,
event,
});
};
onMounted(() => {
watch(
selectedKey,
() => {
const ele = document.getElementById(selectedItemId.value);
if (ele && ele.scrollIntoView) {
ele.scrollIntoView(false);
}
},
{ flush: 'post', immediate: true },
);
});
watch(open, () => {
if (!open.value) {
setSelectedKey(null);
}
});
return () => {
const {
prefixCls,
id,
tabs,
locale,
mobile,
moreIcon = slots.moreIcon?.() || <EllipsisOutlined />,
moreTransitionName,
editable,
tabBarGutter,
rtl,
onTabClick,
} = props;
const dropdownPrefix = `${prefixCls}-dropdown`;
const dropdownAriaLabel = locale?.dropdownAriaLabel;
// ========================= Render =========================
const moreStyle: CSSProperties = {
[rtl ? 'marginRight' : 'marginLeft']: tabBarGutter,
};
if (!tabs.length) {
moreStyle.visibility = 'hidden';
moreStyle.order = 1;
}
const overlayClassName = classNames({
[`${dropdownPrefix}-rtl`]: rtl,
});
const moreNode = mobile ? null : (
<Dropdown
prefixCls={dropdownPrefix}
trigger={['hover']}
visible={open.value}
transitionName={moreTransitionName}
onVisibleChange={setOpen}
overlayClassName={overlayClassName}
mouseEnterDelay={0.1}
mouseLeaveDelay={0.1}
v-slots={{
overlay: () => (
<Menu
onClick={({ key, domEvent }) => {
onTabClick(key, domEvent);
setOpen(false);
}}
id={popupId.value}
tabindex={-1}
role="listbox"
aria-activedescendant={selectedItemId.value}
selectedKeys={[selectedKey.value]}
aria-label={
dropdownAriaLabel !== undefined ? dropdownAriaLabel : 'expanded dropdown'
}
>
{tabs.map(tab => {
const removable = editable && tab.closable !== false && !tab.disabled;
return (
<MenuItem
key={tab.key}
id={`${popupId.value}-${tab.key}`}
role="option"
aria-controls={id && `${id}-panel-${tab.key}`}
disabled={tab.disabled}
>
<span>{typeof tab.tab === 'function' ? tab.tab() : tab.tab}</span>
{removable && (
<button
type="button"
aria-label={props.removeAriaLabel || 'remove'}
tabindex={0}
class={`${dropdownPrefix}-menu-item-remove`}
onClick={e => {
e.stopPropagation();
onRemoveTab(e, tab.key);
}}
>
{tab.closeIcon?.() || editable.removeIcon?.() || '×'}
</button>
)}
</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>
);
};
},
});