Skip to content

Commit 9289b16

Browse files
authored
refactor: use renderable guards for tab content (#1021)
* refactor: use renderable guards * test: cover renderable icon branches * test: always restore timers in overflow test
1 parent 3fcc54e commit 9289b16

7 files changed

Lines changed: 119 additions & 10 deletions

File tree

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"@rc-component/menu": "~1.6.0",
4646
"@rc-component/motion": "^1.1.3",
4747
"@rc-component/resize-observer": "^1.0.0",
48-
"@rc-component/util": "^1.11.1",
48+
"@rc-component/util": "^1.13.0",
4949
"clsx": "^2.1.1"
5050
},
5151
"devDependencies": {

‎src/TabNavList/AddButton.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as React from 'react';
2+
import { isReactRenderable } from '@rc-component/util';
23
import type { EditableConfig, TabsLocale } from '../interface';
34

45
export interface AddButtonProps {
@@ -25,7 +26,7 @@ const AddButton = React.forwardRef<HTMLButtonElement, AddButtonProps>((props, re
2526
editable.onEdit('add', { event });
2627
}}
2728
>
28-
{editable.addIcon || '+'}
29+
{isReactRenderable(editable.addIcon) ? editable.addIcon : '+'}
2930
</button>
3031
);
3132
});

‎src/TabNavList/ExtraContent.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as React from 'react';
2+
import { isReactRenderable } from '@rc-component/util';
23
import type { TabBarExtraContent, TabBarExtraMap, TabBarExtraPosition } from '../interface';
34

45
interface ExtraContentProps {
@@ -9,7 +10,7 @@ interface ExtraContentProps {
910

1011
const ExtraContent = React.forwardRef<HTMLDivElement, ExtraContentProps>((props, ref) => {
1112
const { position, prefixCls, extra } = props;
12-
if (!extra) {
13+
if (!isReactRenderable(extra)) {
1314
return null;
1415
}
1516

@@ -31,7 +32,7 @@ const ExtraContent = React.forwardRef<HTMLDivElement, ExtraContentProps>((props,
3132
content = assertExtra.left;
3233
}
3334

34-
return content ? (
35+
return isReactRenderable(content) ? (
3536
<div className={`${prefixCls}-extra-content`} ref={ref}>
3637
{content}
3738
</div>

‎src/TabNavList/OperationNode.tsx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { clsx } from 'clsx';
22
import Dropdown from '@rc-component/dropdown';
33
import Menu, { MenuItem } from '@rc-component/menu';
4-
import { KeyCode } from '@rc-component/util';
4+
import { isReactRenderable, KeyCode } from '@rc-component/util';
55
import * as React from 'react';
66
import { useEffect, useState } from 'react';
77
import type { EditableConfig, Tab, TabsLocale, MoreProps } from '../interface';
@@ -110,7 +110,11 @@ const OperationNode = React.forwardRef<HTMLDivElement, OperationNodeProps>((prop
110110
onRemoveTab(e, key);
111111
}}
112112
>
113-
{closeIcon || editable.removeIcon || '×'}
113+
{isReactRenderable(closeIcon)
114+
? closeIcon
115+
: isReactRenderable(editable.removeIcon)
116+
? editable.removeIcon
117+
: '×'}
114118
</button>
115119
)}
116120
</MenuItem>

‎src/TabNavList/TabNode.tsx‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { clsx } from 'clsx';
2+
import { isReactRenderable } from '@rc-component/util';
23
import * as React from 'react';
34
import type { EditableConfig, Tab } from '../interface';
45
import { genDataNodeKey, getRemovable } from '../util';
@@ -68,7 +69,7 @@ const TabNode: React.FC<TabNodeProps> = props => {
6869
}
6970

7071
const labelNode = React.useMemo<React.ReactNode>(
71-
() => (icon && typeof label === 'string' ? <span>{label}</span> : label),
72+
() => (isReactRenderable(icon) && typeof label === 'string' ? <span>{label}</span> : label),
7273
[label, icon],
7374
);
7475

@@ -121,8 +122,8 @@ const TabNode: React.FC<TabNodeProps> = props => {
121122
{`Tab ${currentPosition} of ${tabCount}`}
122123
</div>
123124
)}
124-
{icon && <span className={`${tabPrefix}-icon`}>{icon}</span>}
125-
{label && labelNode}
125+
{isReactRenderable(icon) && <span className={`${tabPrefix}-icon`}>{icon}</span>}
126+
{isReactRenderable(label) && labelNode}
126127
</div>
127128

128129
{/* Remove Button */}
@@ -138,7 +139,11 @@ const TabNode: React.FC<TabNodeProps> = props => {
138139
onRemoveTab(e);
139140
}}
140141
>
141-
{closeIcon || editable.removeIcon || '×'}
142+
{isReactRenderable(closeIcon)
143+
? closeIcon
144+
: isReactRenderable(editable.removeIcon)
145+
? editable.removeIcon
146+
: '×'}
142147
</button>
143148
)}
144149
</div>

‎tests/index.test.tsx‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,56 @@ describe('Tabs.Basic', () => {
464464
container.querySelector('.rc-tabs-tab-remove').querySelector('.close-light'),
465465
).toBeTruthy();
466466
});
467+
468+
it('should render numeric close and remove icons', () => {
469+
const onEdit = jest.fn();
470+
const { container } = render(
471+
getTabs({
472+
editable: { onEdit, removeIcon: 0 },
473+
items: [
474+
{
475+
key: 'close-icon',
476+
label: 'Close icon',
477+
closeIcon: 0,
478+
children: 'Close icon',
479+
},
480+
{
481+
key: 'remove-icon',
482+
label: 'Remove icon',
483+
children: 'Remove icon',
484+
},
485+
],
486+
}),
487+
);
488+
489+
const removes = container.querySelectorAll('.rc-tabs-tab-remove');
490+
expect(removes).toHaveLength(2);
491+
expect(removes[0]).toHaveTextContent('0');
492+
expect(removes[1]).toHaveTextContent('0');
493+
});
494+
495+
it.each([false, '', null, undefined])(
496+
'should fall back for a non-renderable remove icon: %p',
497+
removeIcon => {
498+
const onEdit = jest.fn();
499+
const { container } = render(
500+
getTabs({
501+
editable: { onEdit, removeIcon },
502+
items: [
503+
{
504+
key: 'fallback-icon',
505+
label: 'Fallback icon',
506+
closable: true,
507+
children: 'Fallback icon',
508+
},
509+
],
510+
}),
511+
);
512+
513+
expect(container.querySelector('.rc-tabs-tab-remove')).toHaveTextContent('×');
514+
},
515+
);
516+
467517
it('should hide closeIcon when closeIcon is set to null or false', () => {
468518
const onEdit = jest.fn();
469519
const { container } = render(

‎tests/overflow.test.tsx‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,54 @@ describe('Tabs.Overflow', () => {
398398
});
399399
});
400400

401+
it('renders numeric remove icons in the dropdown menu', () => {
402+
jest.useFakeTimers();
403+
try {
404+
hackOffsetInfo.container = 20;
405+
406+
const onEdit = jest.fn();
407+
const { container, unmount } = render(
408+
getTabs({
409+
editable: { onEdit, removeIcon: 0 },
410+
items: [
411+
{
412+
key: 'close-icon',
413+
label: 'Close icon',
414+
closeIcon: 0,
415+
children: 'Close icon',
416+
},
417+
{
418+
key: 'remove-icon',
419+
label: 'Remove icon',
420+
closeIcon: '',
421+
closable: true,
422+
children: 'Remove icon',
423+
},
424+
],
425+
}),
426+
);
427+
428+
triggerResize(container);
429+
act(() => {
430+
jest.runAllTimers();
431+
});
432+
433+
fireEvent.mouseEnter(container.querySelector('.rc-tabs-nav-more'));
434+
act(() => {
435+
jest.runAllTimers();
436+
});
437+
438+
const removes = document.querySelectorAll('.rc-tabs-dropdown-menu-item-remove');
439+
expect(removes).toHaveLength(2);
440+
expect(removes[0]).toHaveTextContent('0');
441+
expect(removes[1]).toHaveTextContent('0');
442+
443+
unmount();
444+
} finally {
445+
jest.useRealTimers();
446+
}
447+
});
448+
401449
it('auto hidden Dropdown', async () => {
402450
jest.useFakeTimers();
403451

0 commit comments

Comments
 (0)