From f41a0457594bd46f14f16f79d249231510a45285 Mon Sep 17 00:00:00 2001 From: ice <1597834867@qq.com> Date: Wed, 24 Sep 2025 15:56:54 +0800 Subject: [PATCH 1/6] feat: add 'close' semantic name and update TabNode styles and classNames --- src/TabNavList/TabNode.tsx | 16 +++++++++------- src/TabNavList/index.tsx | 12 +++++++++--- src/Tabs.tsx | 2 +- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/TabNavList/TabNode.tsx b/src/TabNavList/TabNode.tsx index 8e076898..2946d2ca 100644 --- a/src/TabNavList/TabNode.tsx +++ b/src/TabNavList/TabNode.tsx @@ -2,6 +2,7 @@ import { clsx } from 'clsx'; import * as React from 'react'; import type { EditableConfig, Tab } from '../interface'; import { genDataNodeKey, getRemovable } from '../util'; +import type { SemanticName } from '@/Tabs'; export interface TabNodeProps { id: string; @@ -23,8 +24,8 @@ export interface TabNodeProps { onMouseUp: React.MouseEventHandler; onFocus: React.FocusEventHandler; onBlur: React.FocusEventHandler; - style?: React.CSSProperties; - className?: string; + styles?: Pick, 'item' | 'close'>; + classNames?: Pick, 'item' | 'close'>; } const TabNode: React.FC = props => { @@ -44,8 +45,8 @@ const TabNode: React.FC = props => { onKeyDown, onMouseDown, onMouseUp, - style, - className, + styles, + classNames: tabNodeClassNames, tabCount, currentPosition, } = props; @@ -83,13 +84,13 @@ const TabNode: React.FC = props => {
{/* Primary Tab Button */} @@ -130,7 +131,8 @@ const TabNode: React.FC = props => { type="button" aria-label={removeAriaLabel || 'remove'} tabIndex={active ? 0 : -1} - className={`${tabPrefix}-remove`} + className={clsx(`${tabPrefix}-remove`, tabNodeClassNames?.close)} + style={styles?.close} onClick={e => { e.stopPropagation(); onRemoveTab(e); diff --git a/src/TabNavList/index.tsx b/src/TabNavList/index.tsx index 39ea7cac..1a9f4254 100644 --- a/src/TabNavList/index.tsx +++ b/src/TabNavList/index.tsx @@ -435,9 +435,15 @@ const TabNavList = React.forwardRef((props, ref prefixCls={prefixCls} key={key} tab={tab} - className={tabsClassNames?.item} - /* first node should not have margin left */ - style={i === 0 ? styles?.item : { ...tabNodeStyle, ...styles?.item }} + classNames={{ + item: tabsClassNames?.item, + close: tabsClassNames?.close, + }} + styles={{ + /* first node should not have margin left */ + item: i === 0 ? styles?.item : { ...tabNodeStyle, ...styles?.item }, + close: styles?.close, + }} closable={tab.closable} editable={editable} active={key === activeKey} diff --git a/src/Tabs.tsx b/src/Tabs.tsx index 2dce753a..e90d9a0c 100644 --- a/src/Tabs.tsx +++ b/src/Tabs.tsx @@ -35,7 +35,7 @@ import type { // Used for accessibility let uuid = 0; -export type SemanticName = 'popup' | 'item' | 'indicator' | 'content' | 'header'; +export type SemanticName = 'popup' | 'item' | 'indicator' | 'content' | 'header' | 'close'; export interface TabsProps extends Omit, 'onChange' | 'children'> { From ffc86fd153253aec1b271f6d065f1bf2323ba3fe Mon Sep 17 00:00:00 2001 From: ice <1597834867@qq.com> Date: Wed, 24 Sep 2025 16:12:12 +0800 Subject: [PATCH 2/6] types: update TabNode styles and classNames to use Partial type for better flexibility --- src/TabNavList/TabNode.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TabNavList/TabNode.tsx b/src/TabNavList/TabNode.tsx index 2946d2ca..1ee1e049 100644 --- a/src/TabNavList/TabNode.tsx +++ b/src/TabNavList/TabNode.tsx @@ -24,8 +24,8 @@ export interface TabNodeProps { onMouseUp: React.MouseEventHandler; onFocus: React.FocusEventHandler; onBlur: React.FocusEventHandler; - styles?: Pick, 'item' | 'close'>; - classNames?: Pick, 'item' | 'close'>; + styles?: Pick>, 'item' | 'close'>; + classNames?: Pick>, 'item' | 'close'>; } const TabNode: React.FC = props => { From f0f7e8c523e827af3b245639dfc06c66cec8551d Mon Sep 17 00:00:00 2001 From: ice <1597834867@qq.com> Date: Wed, 24 Sep 2025 17:03:46 +0800 Subject: [PATCH 3/6] test: add unit test for editable Tabs component with custom classNames and styles --- tests/index.test.tsx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 606b4cfe..04948f41 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -775,4 +775,28 @@ describe('Tabs.Basic', () => { expect(content).toHaveStyle({ background: 'green' }); expect(header).toHaveStyle({ background: 'yellow' }); }); + + it('support classnames and styles for editable', () => { + const customClassNames = { + close: 'custom-close', + }; + const customStyles = { + close: { background: 'red' }, + }; + + const { container } = render( + {}, + }} + tabPosition="left" + items={[{ key: 'test', label: 'test', icon: 'test' }]} + styles={customStyles} + classNames={customClassNames} + />, + ); + + expect(container.querySelector('.rc-tabs-tab-remove')).toHaveClass('custom-close'); + expect(container.querySelector('.rc-tabs-tab-remove')).toHaveStyle({ background: 'red' }); + }); }); From 079a6951f7f80b092292e424aa701cb09b011943 Mon Sep 17 00:00:00 2001 From: ice <1597834867@qq.com> Date: Thu, 9 Oct 2025 18:34:20 +0800 Subject: [PATCH 4/6] feat: enhance OperationNode to support custom classNames and styles for close button --- src/TabNavList/OperationNode.tsx | 8 +++++++- tests/index.test.tsx | 33 ++++++++++++++++++++++---------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/TabNavList/OperationNode.tsx b/src/TabNavList/OperationNode.tsx index def12570..8a0c7f59 100644 --- a/src/TabNavList/OperationNode.tsx +++ b/src/TabNavList/OperationNode.tsx @@ -7,6 +7,7 @@ import { useEffect, useState } from 'react'; import type { EditableConfig, Tab, TabsLocale, MoreProps } from '../interface'; import { getRemovable } from '../util'; import AddButton from './AddButton'; +import { SemanticName } from '../Tabs'; export interface OperationNodeProps { prefixCls: string; @@ -27,6 +28,8 @@ export interface OperationNodeProps { getPopupContainer?: (node: HTMLElement) => HTMLElement; popupClassName?: string; popupStyle?: React.CSSProperties; + styles?: Pick>, 'close'>; + classNames?: Pick>, 'close'>; } const OperationNode = React.forwardRef((props, ref) => { @@ -47,6 +50,8 @@ const OperationNode = React.forwardRef((prop getPopupContainer, popupClassName, popupStyle, + classNames, + styles, } = props; // ======================== Dropdown ======================== const [open, setOpen] = useState(false); @@ -98,7 +103,8 @@ const OperationNode = React.forwardRef((prop type="button" aria-label={removeAriaLabel || 'remove'} tabIndex={0} - className={`${dropdownPrefix}-menu-item-remove`} + className={clsx(`${dropdownPrefix}-menu-item-remove`, classNames?.close)} + style={styles?.close} onClick={e => { e.stopPropagation(); onRemoveTab(e, key); diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 04948f41..d769cfb6 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -776,7 +776,7 @@ describe('Tabs.Basic', () => { expect(header).toHaveStyle({ background: 'yellow' }); }); - it('support classnames and styles for editable', () => { + it('support classnames and styles for editable close button', () => { const customClassNames = { close: 'custom-close', }; @@ -785,17 +785,30 @@ describe('Tabs.Basic', () => { }; const { container } = render( - {}, - }} - tabPosition="left" - items={[{ key: 'test', label: 'test', icon: 'test' }]} - styles={customStyles} - classNames={customClassNames} - />, +
+ {}, + }} + tabPosition="left" + items={Array.from({ length: 10 }).map((_, index) => ({ + key: `test-${index}`, + label: `test-${index}`, + icon: 'test', + }))} + styles={customStyles} + classNames={customClassNames} + getPopupContainer={() => document.querySelector('.rc-tabs') as HTMLElement} + /> +
, ); + expect(container.querySelector('.rc-tabs-dropdown-menu-item-remove')).toHaveClass( + 'custom-close', + ); + expect(container.querySelector('.rc-tabs-dropdown-menu-item-remove')).toHaveStyle({ + background: 'red', + }); expect(container.querySelector('.rc-tabs-tab-remove')).toHaveClass('custom-close'); expect(container.querySelector('.rc-tabs-tab-remove')).toHaveStyle({ background: 'red' }); }); From e64d138813e603858e60db14d15990e007a17076 Mon Sep 17 00:00:00 2001 From: ice <49827327+coding-ice@users.noreply.github.com> Date: Thu, 9 Oct 2025 21:28:35 +0800 Subject: [PATCH 5/6] Update src/TabNavList/OperationNode.tsx Co-authored-by: lijianan <574980606@qq.com> --- src/TabNavList/OperationNode.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TabNavList/OperationNode.tsx b/src/TabNavList/OperationNode.tsx index 8a0c7f59..1c83feb8 100644 --- a/src/TabNavList/OperationNode.tsx +++ b/src/TabNavList/OperationNode.tsx @@ -7,7 +7,7 @@ import { useEffect, useState } from 'react'; import type { EditableConfig, Tab, TabsLocale, MoreProps } from '../interface'; import { getRemovable } from '../util'; import AddButton from './AddButton'; -import { SemanticName } from '../Tabs'; +import type { SemanticName } from '../Tabs'; export interface OperationNodeProps { prefixCls: string; From 1b20c6fb50c2e7bf517492c134d247afe1f1f245 Mon Sep 17 00:00:00 2001 From: ice <1597834867@qq.com> Date: Fri, 10 Oct 2025 00:12:55 +0800 Subject: [PATCH 6/6] test: add unit test for editable Tabs component to verify custom classNames and styles for close button --- tests/index.test.tsx | 7 ------- tests/overflow.test.tsx | 27 +++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/tests/index.test.tsx b/tests/index.test.tsx index d769cfb6..58f4980b 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -798,17 +798,10 @@ describe('Tabs.Basic', () => { }))} styles={customStyles} classNames={customClassNames} - getPopupContainer={() => document.querySelector('.rc-tabs') as HTMLElement} />
, ); - expect(container.querySelector('.rc-tabs-dropdown-menu-item-remove')).toHaveClass( - 'custom-close', - ); - expect(container.querySelector('.rc-tabs-dropdown-menu-item-remove')).toHaveStyle({ - background: 'red', - }); expect(container.querySelector('.rc-tabs-tab-remove')).toHaveClass('custom-close'); expect(container.querySelector('.rc-tabs-tab-remove')).toHaveStyle({ background: 'red' }); }); diff --git a/tests/overflow.test.tsx b/tests/overflow.test.tsx index d362fb47..c29f2f28 100644 --- a/tests/overflow.test.tsx +++ b/tests/overflow.test.tsx @@ -528,6 +528,33 @@ describe('Tabs.Overflow', () => { expect(document.querySelector('.rc-tabs-dropdown')).toHaveStyle('color: red'); }); + it('should support classnames and styles for editable close button', () => { + jest.useFakeTimers(); + const { container } = render( + getTabs({ + editable: { onEdit: () => {} }, + classNames: { close: 'custom-close' }, + styles: { close: { color: 'red' } }, + }), + ); + + triggerResize(container); + act(() => { + jest.runAllTimers(); + }); + + fireEvent.mouseEnter(container.querySelector('.rc-tabs-nav-more')); + act(() => { + jest.runAllTimers(); + }); + expect(document.querySelector('.rc-tabs-dropdown-menu-item-remove')).toHaveClass( + 'custom-close', + ); + expect(document.querySelector('.rc-tabs-dropdown-menu-item-remove')).toHaveStyle({ + color: 'red', + }); + }); + it('correct handle decimal', () => { hackOffsetInfo.container = 29; hackOffsetInfo.tabNodeList = 29;