235 lines
7.4 KiB
Vue
235 lines
7.4 KiB
Vue
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>
|
||
);
|
||
};
|
||
},
|
||
});
|