Skip to content

Commit b844e4a

Browse files
authored
fix: Overflow of add button should also show the dropdown (#299)
* refactor * remove overflow add size * fix scroll logic * format number * add test case
1 parent 36e0a77 commit b844e4a

File tree

5 files changed

+129
-31
lines changed

5 files changed

+129
-31
lines changed

examples/mix.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export default () => {
160160
tabBarExtraContent="extra"
161161
defaultActiveKey="30"
162162
moreIcon="..."
163-
moreTransitionName="233"
163+
// moreTransitionName="233"
164164
style={{ height: fixHeight ? 300 : null }}
165165
>
166166
{tabPanes}

src/TabNavList/index.tsx

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,12 @@ function TabNavList(props: TabNavListProps, ref: React.Ref<HTMLDivElement>) {
8484

8585
const [wrapperScrollWidth, setWrapperScrollWidth] = useState<number>(0);
8686
const [wrapperScrollHeight, setWrapperScrollHeight] = useState<number>(0);
87+
const [wrapperContentWidth, setWrapperContentWidth] = useState<number>(0);
88+
const [wrapperContentHeight, setWrapperContentHeight] = useState<number>(0);
8789
const [wrapperWidth, setWrapperWidth] = useState<number>(null);
8890
const [wrapperHeight, setWrapperHeight] = useState<number>(null);
91+
const [addWidth, setAddWidth] = useState<number>(0);
92+
const [addHeight, setAddHeight] = useState<number>(0);
8993

9094
const [tabSizes, setTabSizes] = useRafState<TabSizeMap>(new Map());
9195
const tabOffsets = useOffsets(tabs, tabSizes, wrapperScrollWidth);
@@ -226,19 +230,17 @@ function TabNavList(props: TabNavListProps, ref: React.Ref<HTMLDivElement>) {
226230
left: transformLeft,
227231
top: transformTop,
228232
},
233+
{
234+
width: wrapperContentWidth,
235+
height: wrapperContentHeight,
236+
},
237+
{
238+
width: addWidth,
239+
height: addHeight,
240+
},
229241
{ ...props, tabs },
230242
);
231243

232-
function getAdditionalSpaceSize(type: 'offsetWidth' | 'offsetHeight') {
233-
const addBtnSize = innerAddButtonRef.current?.[type] || 0;
234-
let optionsSize = 0;
235-
if (operationsRef.current?.className.includes(operationsHiddenClassName)) {
236-
optionsSize = operationsRef.current[type];
237-
}
238-
239-
return addBtnSize + optionsSize;
240-
}
241-
242244
const tabNodes: React.ReactElement[] = tabs.map(tab => {
243245
const { key } = tab;
244246
return (
@@ -280,14 +282,25 @@ function TabNavList(props: TabNavListProps, ref: React.Ref<HTMLDivElement>) {
280282
// Update wrapper records
281283
const offsetWidth = tabsWrapperRef.current?.offsetWidth || 0;
282284
const offsetHeight = tabsWrapperRef.current?.offsetHeight || 0;
285+
const newAddWidth = innerAddButtonRef.current?.offsetWidth || 0;
286+
const newAddHeight = innerAddButtonRef.current?.offsetHeight || 0;
287+
const newOperationWidth = operationsRef.current?.offsetWidth || 0;
288+
const newOperationHeight = operationsRef.current?.offsetHeight || 0;
289+
283290
setWrapperWidth(offsetWidth);
284291
setWrapperHeight(offsetHeight);
285-
setWrapperScrollWidth(
286-
(tabListRef.current?.offsetWidth || 0) - getAdditionalSpaceSize('offsetWidth'),
287-
);
288-
setWrapperScrollHeight(
289-
(tabListRef.current?.offsetHeight || 0) - getAdditionalSpaceSize('offsetHeight'),
290-
);
292+
setAddWidth(newAddWidth);
293+
setAddHeight(newAddHeight);
294+
295+
const newWrapperScrollWidth = (tabListRef.current?.offsetWidth || 0) - newAddWidth;
296+
const newWrapperScrollHeight = (tabListRef.current?.offsetHeight || 0) - newAddHeight;
297+
298+
setWrapperScrollWidth(newWrapperScrollWidth);
299+
setWrapperScrollHeight(newWrapperScrollHeight);
300+
301+
const isOperationHidden = operationsRef.current?.className.includes(operationsHiddenClassName);
302+
setWrapperContentWidth(newWrapperScrollWidth - (isOperationHidden ? 0 : newOperationWidth));
303+
setWrapperContentHeight(newWrapperScrollHeight - (isOperationHidden ? 0 : newOperationHeight));
291304

292305
// Update buttons records
293306
setTabSizes(() => {
@@ -355,7 +368,7 @@ function TabNavList(props: TabNavListProps, ref: React.Ref<HTMLDivElement>) {
355368
// Should recalculate when rtl changed
356369
useEffect(() => {
357370
onListHolderResize();
358-
}, [rtl, tabBarGutter, activeKey, tabs.map((tab) => tab.key).join('_')]);
371+
}, [rtl, tabBarGutter, activeKey, tabs.map(tab => tab.key).join('_')]);
359372

360373
// ========================= Render ========================
361374
const hasDropdown = !!hiddenTabs.length;
@@ -410,14 +423,13 @@ function TabNavList(props: TabNavListProps, ref: React.Ref<HTMLDivElement>) {
410423
}}
411424
>
412425
{tabNodes}
413-
{!hasDropdown && (
414-
<AddButton
415-
ref={innerAddButtonRef}
416-
prefixCls={prefixCls}
417-
locale={locale}
418-
editable={editable}
419-
/>
420-
)}
426+
<AddButton
427+
ref={innerAddButtonRef}
428+
prefixCls={prefixCls}
429+
locale={locale}
430+
editable={editable}
431+
style={{ visibility: hasDropdown ? 'hidden' : null }}
432+
/>
421433

422434
<div
423435
className={classNames(`${prefixCls}-ink-bar`, {

src/hooks/useVisibleRange.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const DEFAULT_SIZE = { width: 0, height: 0, left: 0, top: 0, right: 0 };
77
export default function useVisibleRange(
88
tabOffsets: TabOffsetMap,
99
containerSize: { width: number; height: number; left: number; top: number },
10+
tabContentNodeSize: { width: number; height: number },
11+
addNodeSize: { width: number; height: number },
1012
{ tabs, tabPosition, rtl }: { tabs: Tab[] } & TabNavListProps,
1113
): [number, number] {
1214
let unit: 'width' | 'height';
@@ -24,6 +26,13 @@ export default function useVisibleRange(
2426
}
2527

2628
const basicSize = containerSize[unit];
29+
const tabContentSize = tabContentNodeSize[unit];
30+
const addSize = addNodeSize[unit];
31+
32+
let mergedBasicSize = basicSize;
33+
if (tabContentSize + addSize > basicSize) {
34+
mergedBasicSize = basicSize - addSize;
35+
}
2736

2837
return useMemo(() => {
2938
if (!tabs.length) {
@@ -34,7 +43,7 @@ export default function useVisibleRange(
3443
let endIndex = len;
3544
for (let i = 0; i < len; i += 1) {
3645
const offset = tabOffsets.get(tabs[i].key) || DEFAULT_SIZE;
37-
if (offset[position] + offset[unit] > transformSize + basicSize) {
46+
if (offset[position] + offset[unit] > transformSize + mergedBasicSize) {
3847
endIndex = i - 1;
3948
break;
4049
}
@@ -50,5 +59,12 @@ export default function useVisibleRange(
5059
}
5160

5261
return [startIndex, endIndex];
53-
}, [tabOffsets, transformSize, basicSize, tabPosition, tabs.map(tab => tab.key).join('_'), rtl]);
62+
}, [
63+
tabOffsets,
64+
transformSize,
65+
mergedBasicSize,
66+
tabPosition,
67+
tabs.map(tab => tab.key).join('_'),
68+
rtl,
69+
]);
5470
}

tests/common/util.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { ReactWrapper } from 'enzyme';
33
import Tabs, { TabPane } from '../../src';
44
import { TabsProps } from '../../src/Tabs';
55

6-
export function getOffsetSizeFunc(info: any = {}) {
6+
export function getOffsetSizeFunc(
7+
info: { list?: number; wrapper?: number; add?: number; operation?: number } = {},
8+
) {
79
return function getOffsetSize() {
810
if (this.className.includes('rc-tabs-tab')) {
911
return 20;
@@ -12,10 +14,13 @@ export function getOffsetSizeFunc(info: any = {}) {
1214
return info.list || 5 * 20;
1315
}
1416
if (this.className.includes('rc-tabs-nav-wrap')) {
15-
return 40;
17+
return info.wrapper || 40;
18+
}
19+
if (this.className.includes('rc-tabs-nav-add')) {
20+
return info.add || 10;
1621
}
1722
if (this.className.includes('rc-tabs-nav-operations')) {
18-
return 10;
23+
return info.operation || 10;
1924
}
2025

2126
throw new Error(`className not match ${this.className}`);

tests/operation-overflow.test.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { mount } from 'enzyme';
2+
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
3+
import { act } from 'react-dom/test-utils';
4+
import { getOffsetSizeFunc, getTabs, triggerResize } from './common/util';
5+
6+
describe('Tabs.Operation-Overflow', () => {
7+
let domSpy: ReturnType<typeof spyElementPrototypes>;
8+
let holder: HTMLDivElement;
9+
10+
const hackOffsetInfo = { wrapper: 105, list: 110, add: 10, operation: 20 };
11+
12+
beforeAll(() => {
13+
holder = document.createElement('div');
14+
document.body.appendChild(holder);
15+
16+
function btnOffsetPosition() {
17+
const btn = this as HTMLButtonElement;
18+
const btnList = [...btn.parentNode.childNodes].filter(ele =>
19+
(ele as HTMLElement).className.includes('rc-tabs-tab'),
20+
);
21+
const index = btnList.indexOf(btn);
22+
return 20 * index;
23+
}
24+
25+
domSpy = spyElementPrototypes(HTMLElement, {
26+
scrollIntoView: () => {},
27+
offsetWidth: {
28+
get: getOffsetSizeFunc(hackOffsetInfo),
29+
},
30+
offsetHeight: {
31+
get: getOffsetSizeFunc(hackOffsetInfo),
32+
},
33+
offsetLeft: {
34+
get: btnOffsetPosition,
35+
},
36+
offsetTop: {
37+
get: btnOffsetPosition,
38+
},
39+
});
40+
});
41+
42+
afterAll(() => {
43+
domSpy.mockRestore();
44+
document.body.removeChild(holder);
45+
});
46+
47+
it('should collapse', () => {
48+
jest.useFakeTimers();
49+
const onEdit = jest.fn();
50+
const wrapper = mount(getTabs({ editable: { onEdit } }));
51+
52+
triggerResize(wrapper);
53+
act(() => {
54+
jest.runAllTimers();
55+
wrapper.update();
56+
});
57+
expect(
58+
wrapper.find('.rc-tabs-nav-operations').hasClass('rc-tabs-nav-operations-hidden'),
59+
).toBeFalsy();
60+
61+
wrapper.unmount();
62+
63+
jest.useRealTimers();
64+
});
65+
});

0 commit comments

Comments
 (0)