Skip to content

Commit 755768b

Browse files
刘欢claude
andcommitted
feat: add popupRender for more popup customization
- Add PopupRender type with tabs, activeKey, and onClose - Implement popupRender in OperationNode - Add demo example for search in overflow menu - Remove old showSearch implementation and tests Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 62751a7 commit 755768b

8 files changed

Lines changed: 119 additions & 433 deletions

File tree

docs/demo/search-dropdown.md

Lines changed: 0 additions & 8 deletions
This file was deleted.

docs/demo/search-overflow.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: Search Overflow
3+
nav:
4+
title: Demo
5+
path: /demo
6+
---
7+
8+
<code src="../examples/search-overflow.tsx"></code>

docs/examples/search-dropdown.tsx

Lines changed: 0 additions & 84 deletions
This file was deleted.

docs/examples/search-overflow.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React, { useState, useMemo } from 'react';
2+
import Tabs from '../../src';
3+
import type { TabsProps, PopupRender } from '../../src';
4+
import Menu, { MenuItem as MenuItemNode } from '@rc-component/menu';
5+
6+
const items: TabsProps['items'] = [];
7+
8+
for (let i = 0; i < 15; i += 1) {
9+
items.push({ key: String(i), label: `Tab ${i + 1}`, children: `Content of ${i}` });
10+
}
11+
12+
export default () => {
13+
const [activeKey, setActiveKey] = useState('0');
14+
const [searchValue, setSearchValue] = useState('');
15+
16+
const popupRender: PopupRender = (menu, { tabs, onClose }) => {
17+
const filteredTabs = useMemo(() => {
18+
if (!searchValue) return tabs;
19+
return tabs.filter(tab =>
20+
String(tab.label).toLowerCase().includes(searchValue.toLowerCase()),
21+
);
22+
}, [tabs, searchValue]);
23+
24+
return (
25+
<div style={{ padding: '8px' }}>
26+
<input
27+
placeholder="搜索 Tab..."
28+
value={searchValue}
29+
onChange={e => setSearchValue(e.target.value)}
30+
style={{
31+
width: '100%',
32+
padding: '4px 8px',
33+
marginBottom: '8px',
34+
border: '1px solid #d9d9d9',
35+
borderRadius: '4px',
36+
outline: 'none',
37+
boxSizing: 'border-box',
38+
}}
39+
onClick={e => e.stopPropagation()}
40+
/>
41+
<Menu
42+
activeKey={activeKey}
43+
onClick={({ key }) => {
44+
setActiveKey(key);
45+
onClose();
46+
}}
47+
>
48+
{filteredTabs.map(tab => (
49+
<MenuItemNode key={tab.key} disabled={tab.disabled}>
50+
{tab.label}
51+
</MenuItemNode>
52+
))}
53+
</Menu>
54+
</div>
55+
);
56+
};
57+
58+
return (
59+
<div style={{ maxWidth: 550 }}>
60+
<Tabs
61+
activeKey={activeKey}
62+
onChange={setActiveKey}
63+
items={items}
64+
more={{
65+
trigger: 'click',
66+
popupRender,
67+
}}
68+
/>
69+
</div>
70+
);
71+
};

src/TabNavList/OperationNode.tsx

Lines changed: 30 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Dropdown from '@rc-component/dropdown';
33
import Menu, { MenuItem } from '@rc-component/menu';
44
import { KeyCode } from '@rc-component/util';
55
import * as React from 'react';
6-
import { useEffect, useRef, useState } from 'react';
6+
import { useEffect, useState } from 'react';
77
import type { EditableConfig, Tab, TabsLocale, MoreProps } from '../interface';
88
import { getRemovable } from '../util';
99
import AddButton from './AddButton';
@@ -39,7 +39,6 @@ const OperationNode = React.forwardRef<HTMLDivElement, OperationNodeProps>((prop
3939
tabs,
4040
locale,
4141
mobile,
42-
activeKey,
4342
more: moreProps = {},
4443
style,
4544
className,
@@ -57,36 +56,8 @@ const OperationNode = React.forwardRef<HTMLDivElement, OperationNodeProps>((prop
5756
// ======================== Dropdown ========================
5857
const [open, setOpen] = useState(false);
5958
const [selectedKey, setSelectedKey] = useState<string>(null);
60-
const [searchValue, setSearchValue] = useState('');
61-
const searchInputRef = useRef<HTMLInputElement>(null);
6259

63-
const { icon: moreIcon = 'More', showSearch } = moreProps;
64-
65-
const isSearchable = !!showSearch;
66-
const showSearchConfig = typeof showSearch === 'object' ? showSearch : {};
67-
const {
68-
placeholder = 'Search',
69-
onSearch,
70-
searchValue: controlledSearchValue,
71-
autoClearSearchValue = true,
72-
filter: filterOption,
73-
} = showSearchConfig;
74-
75-
const mergedSearchValue =
76-
controlledSearchValue !== undefined ? controlledSearchValue : searchValue;
77-
const setSearchValueFn = controlledSearchValue !== undefined ? () => {} : setSearchValue;
78-
79-
const filteredTabs = mergedSearchValue
80-
? tabs.filter(tab => {
81-
if (filterOption) {
82-
return filterOption(tab, mergedSearchValue);
83-
}
84-
if (typeof tab.label === 'string') {
85-
return tab.label.toLowerCase().includes(mergedSearchValue.toLowerCase());
86-
}
87-
return false;
88-
})
89-
: tabs;
60+
const { icon: moreIcon = 'More' } = moreProps;
9061

9162
const popupId = `${id}-more-popup`;
9263
const dropdownPrefix = `${prefixCls}-dropdown`;
@@ -114,7 +85,7 @@ const OperationNode = React.forwardRef<HTMLDivElement, OperationNodeProps>((prop
11485
selectedKeys={[selectedKey]}
11586
aria-label={dropdownAriaLabel !== undefined ? dropdownAriaLabel : 'expanded dropdown'}
11687
>
117-
{filteredTabs.map<React.ReactNode>(tab => {
88+
{tabs.map<React.ReactNode>(tab => {
11889
const { closable, disabled, closeIcon, key, label } = tab;
11990
const removable = getRemovable(closable, closeIcon, editable, disabled);
12091
return (
@@ -148,13 +119,22 @@ const OperationNode = React.forwardRef<HTMLDivElement, OperationNodeProps>((prop
148119
</Menu>
149120
);
150121

122+
// Handle popupRender to allow custom popup content
123+
const { popupRender } = moreProps;
124+
const handleClose = () => setOpen(false);
125+
const overlay = popupRender
126+
? popupRender(menu, {
127+
tabs,
128+
activeKey: props.activeKey,
129+
onClose: handleClose,
130+
})
131+
: menu;
132+
151133
function selectOffset(offset: -1 | 1) {
152-
const enabledTabs = filteredTabs.filter(tab => !tab.disabled);
134+
const enabledTabs = tabs.filter(tab => !tab.disabled);
153135
let selectedIndex = enabledTabs.findIndex(tab => tab.key === selectedKey) || 0;
154136
const len = enabledTabs.length;
155137

156-
if (len === 0) return;
157-
158138
for (let i = 0; i < len; i += 1) {
159139
selectedIndex = (selectedIndex + offset + len) % len;
160140
const tab = enabledTabs[selectedIndex];
@@ -165,9 +145,17 @@ const OperationNode = React.forwardRef<HTMLDivElement, OperationNodeProps>((prop
165145
}
166146
}
167147

168-
function onKeyboardNavigation(e: React.KeyboardEvent) {
148+
function onKeyDown(e: React.KeyboardEvent) {
169149
const { which } = e;
170150

151+
if (!open) {
152+
if ([KeyCode.DOWN, KeyCode.SPACE, KeyCode.ENTER].includes(which)) {
153+
setOpen(true);
154+
e.preventDefault();
155+
}
156+
return;
157+
}
158+
171159
switch (which) {
172160
case KeyCode.UP:
173161
selectOffset(-1);
@@ -182,72 +170,27 @@ const OperationNode = React.forwardRef<HTMLDivElement, OperationNodeProps>((prop
182170
break;
183171
case KeyCode.SPACE:
184172
case KeyCode.ENTER:
185-
if (selectedKey && filteredTabs.some(t => t.key === selectedKey)) {
173+
if (selectedKey !== null) {
186174
onTabClick(selectedKey, e);
187175
}
188176
break;
189177
}
190178
}
191179

192-
function onKeyDown(e: React.KeyboardEvent) {
193-
const { which } = e;
194-
195-
if (!open) {
196-
if ([KeyCode.DOWN, KeyCode.SPACE, KeyCode.ENTER].includes(which)) {
197-
setOpen(true);
198-
e.preventDefault();
199-
}
200-
return;
201-
}
202-
203-
onKeyboardNavigation(e);
204-
}
205-
206-
// 搜索框
207-
const searchInput = isSearchable ? (
208-
<div className={`${dropdownPrefix}-search`}>
209-
<input
210-
ref={searchInputRef}
211-
type="text"
212-
placeholder={placeholder}
213-
value={mergedSearchValue}
214-
onChange={e => {
215-
const value = e.target.value;
216-
setSearchValueFn(value);
217-
onSearch?.(value);
218-
}}
219-
onKeyDown={onKeyboardNavigation}
220-
/>
221-
</div>
222-
) : null;
223-
224180
// ========================= Effect =========================
225181
useEffect(() => {
226182
// We use query element here to avoid React strict warning
227183
const ele = document.getElementById(selectedItemId);
228184
if (ele?.scrollIntoView) {
229-
ele.scrollIntoView({ block: 'center', behavior: 'smooth' });
185+
ele.scrollIntoView(false);
230186
}
231187
}, [selectedItemId, selectedKey]);
232188

233189
useEffect(() => {
234-
if (open) {
235-
if (!selectedKey && activeKey) {
236-
setSelectedKey(activeKey);
237-
}
238-
239-
if (isSearchable) {
240-
requestAnimationFrame(() => {
241-
searchInputRef.current?.focus();
242-
});
243-
}
244-
} else {
190+
if (!open) {
245191
setSelectedKey(null);
246-
if (autoClearSearchValue && controlledSearchValue === undefined) {
247-
setSearchValue('');
248-
}
249192
}
250-
}, [open, activeKey, isSearchable, autoClearSearchValue, controlledSearchValue]);
193+
}, [open]);
251194

252195
// ========================= Render =========================
253196
const moreStyle: React.CSSProperties = {
@@ -261,29 +204,18 @@ const OperationNode = React.forwardRef<HTMLDivElement, OperationNodeProps>((prop
261204

262205
const overlayClassName = clsx(popupClassName, { [`${dropdownPrefix}-rtl`]: rtl });
263206

264-
const dropdownContent = isSearchable ? (
265-
<div className={`${dropdownPrefix}-container`}>
266-
{searchInput}
267-
{menu}
268-
</div>
269-
) : (
270-
menu
271-
);
272-
273-
const { showSearch: _s, ...dropdownProps } = moreProps;
274-
275207
const moreNode: React.ReactNode = mobile ? null : (
276208
<Dropdown
277209
prefixCls={dropdownPrefix}
278-
overlay={dropdownContent}
210+
overlay={overlay}
279211
visible={tabs.length ? open : false}
280212
onVisibleChange={setOpen}
281213
overlayClassName={overlayClassName}
282214
overlayStyle={popupStyle}
283215
mouseEnterDelay={0.1}
284216
mouseLeaveDelay={0.1}
285217
getPopupContainer={getPopupContainer}
286-
{...dropdownProps}
218+
{...moreProps}
287219
>
288220
<button
289221
type="button"

0 commit comments

Comments
 (0)