{}}
+ items={items}
+ more={{
+ showSearch: {
+ placeholder: 'Controlled search...',
+ searchValue,
+ onSearch: setSearchValue,
+ },
+ }}
+ />
+ );
+};
+
+export default () => {
+ const [activeKey, setActiveKey] = useState('1');
+
+ // Generate many tabs to trigger the "more" button
+ const items = Array.from({ length: 30 }, (_, i) => ({
+ key: String(i + 1),
+ label: `Tab ${i + 1}`,
+ children: `Content of Tab ${i + 1}`,
+ }));
+
+ return (
+
+
Basic Usage
+
+
+ Controlled Mode
+
+
+ Keep Search Value on Close
+
+
+ filter
+ ({
+ key: String(i + 1),
+ label: Tab {i + 1},
+ children: `Content of Tab ${i + 1}`,
+ }))}
+ more={{
+ trigger: 'click',
+ showSearch: {
+ placeholder: 'Keep search value',
+ filter: (tab, value) => tab.key.includes(value),
+ },
+ }}
+ />
+
+ );
+};
diff --git a/src/TabNavList/OperationNode.tsx b/src/TabNavList/OperationNode.tsx
index a56ee5a9..d6704605 100644
--- a/src/TabNavList/OperationNode.tsx
+++ b/src/TabNavList/OperationNode.tsx
@@ -3,7 +3,7 @@ import Dropdown from '@rc-component/dropdown';
import Menu, { MenuItem } from '@rc-component/menu';
import { KeyCode } from '@rc-component/util';
import * as React from 'react';
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import type { EditableConfig, Tab, TabsLocale, MoreProps } from '../interface';
import { getRemovable } from '../util';
import AddButton from './AddButton';
@@ -39,6 +39,7 @@ const OperationNode = React.forwardRef((prop
tabs,
locale,
mobile,
+ activeKey,
more: moreProps = {},
style,
className,
@@ -56,8 +57,36 @@ const OperationNode = React.forwardRef((prop
// ======================== Dropdown ========================
const [open, setOpen] = useState(false);
const [selectedKey, setSelectedKey] = useState(null);
+ const [searchValue, setSearchValue] = useState('');
+ const searchInputRef = useRef(null);
- const { icon: moreIcon = 'More' } = moreProps;
+ const { icon: moreIcon = 'More', showSearch } = moreProps;
+
+ const isSearchable = !!showSearch;
+ const showSearchConfig = typeof showSearch === 'object' ? showSearch : {};
+ const {
+ placeholder = 'Search',
+ onSearch,
+ searchValue: controlledSearchValue,
+ autoClearSearchValue = true,
+ filter: filterOption,
+ } = showSearchConfig;
+
+ const mergedSearchValue =
+ controlledSearchValue !== undefined ? controlledSearchValue : searchValue;
+ const setSearchValueFn = controlledSearchValue !== undefined ? () => {} : setSearchValue;
+
+ const filteredTabs = mergedSearchValue
+ ? tabs.filter(tab => {
+ if (filterOption) {
+ return filterOption(tab, mergedSearchValue);
+ }
+ if (typeof tab.label === 'string') {
+ return tab.label.toLowerCase().includes(mergedSearchValue.toLowerCase());
+ }
+ return false;
+ })
+ : tabs;
const popupId = `${id}-more-popup`;
const dropdownPrefix = `${prefixCls}-dropdown`;
@@ -85,7 +114,7 @@ const OperationNode = React.forwardRef((prop
selectedKeys={[selectedKey]}
aria-label={dropdownAriaLabel !== undefined ? dropdownAriaLabel : 'expanded dropdown'}
>
- {tabs.map(tab => {
+ {filteredTabs.map(tab => {
const { closable, disabled, closeIcon, key, label } = tab;
const removable = getRemovable(closable, closeIcon, editable, disabled);
return (
@@ -120,10 +149,12 @@ const OperationNode = React.forwardRef((prop
);
function selectOffset(offset: -1 | 1) {
- const enabledTabs = tabs.filter(tab => !tab.disabled);
+ const enabledTabs = filteredTabs.filter(tab => !tab.disabled);
let selectedIndex = enabledTabs.findIndex(tab => tab.key === selectedKey) || 0;
const len = enabledTabs.length;
+ if (len === 0) return;
+
for (let i = 0; i < len; i += 1) {
selectedIndex = (selectedIndex + offset + len) % len;
const tab = enabledTabs[selectedIndex];
@@ -134,17 +165,9 @@ const OperationNode = React.forwardRef((prop
}
}
- function onKeyDown(e: React.KeyboardEvent) {
+ function onKeyboardNavigation(e: React.KeyboardEvent) {
const { which } = e;
- if (!open) {
- if ([KeyCode.DOWN, KeyCode.SPACE, KeyCode.ENTER].includes(which)) {
- setOpen(true);
- e.preventDefault();
- }
- return;
- }
-
switch (which) {
case KeyCode.UP:
selectOffset(-1);
@@ -159,27 +182,72 @@ const OperationNode = React.forwardRef((prop
break;
case KeyCode.SPACE:
case KeyCode.ENTER:
- if (selectedKey !== null) {
+ if (selectedKey && filteredTabs.some(t => t.key === selectedKey)) {
onTabClick(selectedKey, e);
}
break;
}
}
+ function onKeyDown(e: React.KeyboardEvent) {
+ const { which } = e;
+
+ if (!open) {
+ if ([KeyCode.DOWN, KeyCode.SPACE, KeyCode.ENTER].includes(which)) {
+ setOpen(true);
+ e.preventDefault();
+ }
+ return;
+ }
+
+ onKeyboardNavigation(e);
+ }
+
+ // 搜索框
+ const searchInput = isSearchable ? (
+
+ {
+ const value = e.target.value;
+ setSearchValueFn(value);
+ onSearch?.(value);
+ }}
+ onKeyDown={onKeyboardNavigation}
+ />
+
+ ) : null;
+
// ========================= Effect =========================
useEffect(() => {
// We use query element here to avoid React strict warning
const ele = document.getElementById(selectedItemId);
if (ele?.scrollIntoView) {
- ele.scrollIntoView(false);
+ ele.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
}, [selectedItemId, selectedKey]);
useEffect(() => {
- if (!open) {
+ if (open) {
+ if (!selectedKey && activeKey) {
+ setSelectedKey(activeKey);
+ }
+
+ if (isSearchable) {
+ requestAnimationFrame(() => {
+ searchInputRef.current?.focus();
+ });
+ }
+ } else {
setSelectedKey(null);
+ if (autoClearSearchValue && controlledSearchValue === undefined) {
+ setSearchValue('');
+ }
}
- }, [open]);
+ }, [open, activeKey, isSearchable, autoClearSearchValue, controlledSearchValue]);
// ========================= Render =========================
const moreStyle: React.CSSProperties = {
@@ -193,10 +261,21 @@ const OperationNode = React.forwardRef((prop
const overlayClassName = clsx(popupClassName, { [`${dropdownPrefix}-rtl`]: rtl });
+ const dropdownContent = isSearchable ? (
+
+ {searchInput}
+ {menu}
+
+ ) : (
+ menu
+ );
+
+ const { showSearch: _s, ...dropdownProps } = moreProps;
+
const moreNode: React.ReactNode = mobile ? null : (
((prop
mouseEnterDelay={0.1}
mouseLeaveDelay={0.1}
getPopupContainer={getPopupContainer}
- {...moreProps}
+ {...dropdownProps}
>