diff --git a/examples/mix.tsx b/examples/mix.tsx index 36d7cce5..ead650f5 100644 --- a/examples/mix.tsx +++ b/examples/mix.tsx @@ -160,7 +160,7 @@ export default () => { tabBarExtraContent="extra" defaultActiveKey="30" moreIcon="..." - moreTransitionName="233" + // moreTransitionName="233" style={{ height: fixHeight ? 300 : null }} > {tabPanes} diff --git a/src/TabNavList/index.tsx b/src/TabNavList/index.tsx index 83901f06..5f1a1d22 100644 --- a/src/TabNavList/index.tsx +++ b/src/TabNavList/index.tsx @@ -84,8 +84,12 @@ function TabNavList(props: TabNavListProps, ref: React.Ref) { const [wrapperScrollWidth, setWrapperScrollWidth] = useState(0); const [wrapperScrollHeight, setWrapperScrollHeight] = useState(0); + const [wrapperContentWidth, setWrapperContentWidth] = useState(0); + const [wrapperContentHeight, setWrapperContentHeight] = useState(0); const [wrapperWidth, setWrapperWidth] = useState(null); const [wrapperHeight, setWrapperHeight] = useState(null); + const [addWidth, setAddWidth] = useState(0); + const [addHeight, setAddHeight] = useState(0); const [tabSizes, setTabSizes] = useRafState(new Map()); const tabOffsets = useOffsets(tabs, tabSizes, wrapperScrollWidth); @@ -226,19 +230,17 @@ function TabNavList(props: TabNavListProps, ref: React.Ref) { left: transformLeft, top: transformTop, }, + { + width: wrapperContentWidth, + height: wrapperContentHeight, + }, + { + width: addWidth, + height: addHeight, + }, { ...props, tabs }, ); - function getAdditionalSpaceSize(type: 'offsetWidth' | 'offsetHeight') { - const addBtnSize = innerAddButtonRef.current?.[type] || 0; - let optionsSize = 0; - if (operationsRef.current?.className.includes(operationsHiddenClassName)) { - optionsSize = operationsRef.current[type]; - } - - return addBtnSize + optionsSize; - } - const tabNodes: React.ReactElement[] = tabs.map(tab => { const { key } = tab; return ( @@ -280,14 +282,25 @@ function TabNavList(props: TabNavListProps, ref: React.Ref) { // Update wrapper records const offsetWidth = tabsWrapperRef.current?.offsetWidth || 0; const offsetHeight = tabsWrapperRef.current?.offsetHeight || 0; + const newAddWidth = innerAddButtonRef.current?.offsetWidth || 0; + const newAddHeight = innerAddButtonRef.current?.offsetHeight || 0; + const newOperationWidth = operationsRef.current?.offsetWidth || 0; + const newOperationHeight = operationsRef.current?.offsetHeight || 0; + setWrapperWidth(offsetWidth); setWrapperHeight(offsetHeight); - setWrapperScrollWidth( - (tabListRef.current?.offsetWidth || 0) - getAdditionalSpaceSize('offsetWidth'), - ); - setWrapperScrollHeight( - (tabListRef.current?.offsetHeight || 0) - getAdditionalSpaceSize('offsetHeight'), - ); + setAddWidth(newAddWidth); + setAddHeight(newAddHeight); + + const newWrapperScrollWidth = (tabListRef.current?.offsetWidth || 0) - newAddWidth; + const newWrapperScrollHeight = (tabListRef.current?.offsetHeight || 0) - newAddHeight; + + setWrapperScrollWidth(newWrapperScrollWidth); + setWrapperScrollHeight(newWrapperScrollHeight); + + const isOperationHidden = operationsRef.current?.className.includes(operationsHiddenClassName); + setWrapperContentWidth(newWrapperScrollWidth - (isOperationHidden ? 0 : newOperationWidth)); + setWrapperContentHeight(newWrapperScrollHeight - (isOperationHidden ? 0 : newOperationHeight)); // Update buttons records setTabSizes(() => { @@ -355,7 +368,7 @@ function TabNavList(props: TabNavListProps, ref: React.Ref) { // Should recalculate when rtl changed useEffect(() => { onListHolderResize(); - }, [rtl, tabBarGutter, activeKey, tabs.map((tab) => tab.key).join('_')]); + }, [rtl, tabBarGutter, activeKey, tabs.map(tab => tab.key).join('_')]); // ========================= Render ======================== const hasDropdown = !!hiddenTabs.length; @@ -410,14 +423,13 @@ function TabNavList(props: TabNavListProps, ref: React.Ref) { }} > {tabNodes} - {!hasDropdown && ( - - )} +
basicSize) { + mergedBasicSize = basicSize - addSize; + } return useMemo(() => { if (!tabs.length) { @@ -34,7 +43,7 @@ export default function useVisibleRange( let endIndex = len; for (let i = 0; i < len; i += 1) { const offset = tabOffsets.get(tabs[i].key) || DEFAULT_SIZE; - if (offset[position] + offset[unit] > transformSize + basicSize) { + if (offset[position] + offset[unit] > transformSize + mergedBasicSize) { endIndex = i - 1; break; } @@ -50,5 +59,12 @@ export default function useVisibleRange( } return [startIndex, endIndex]; - }, [tabOffsets, transformSize, basicSize, tabPosition, tabs.map(tab => tab.key).join('_'), rtl]); + }, [ + tabOffsets, + transformSize, + mergedBasicSize, + tabPosition, + tabs.map(tab => tab.key).join('_'), + rtl, + ]); } diff --git a/tests/common/util.tsx b/tests/common/util.tsx index cdbda8d7..491af9a5 100644 --- a/tests/common/util.tsx +++ b/tests/common/util.tsx @@ -3,7 +3,9 @@ import { ReactWrapper } from 'enzyme'; import Tabs, { TabPane } from '../../src'; import { TabsProps } from '../../src/Tabs'; -export function getOffsetSizeFunc(info: any = {}) { +export function getOffsetSizeFunc( + info: { list?: number; wrapper?: number; add?: number; operation?: number } = {}, +) { return function getOffsetSize() { if (this.className.includes('rc-tabs-tab')) { return 20; @@ -12,10 +14,13 @@ export function getOffsetSizeFunc(info: any = {}) { return info.list || 5 * 20; } if (this.className.includes('rc-tabs-nav-wrap')) { - return 40; + return info.wrapper || 40; + } + if (this.className.includes('rc-tabs-nav-add')) { + return info.add || 10; } if (this.className.includes('rc-tabs-nav-operations')) { - return 10; + return info.operation || 10; } throw new Error(`className not match ${this.className}`); diff --git a/tests/operation-overflow.test.tsx b/tests/operation-overflow.test.tsx new file mode 100644 index 00000000..aac79540 --- /dev/null +++ b/tests/operation-overflow.test.tsx @@ -0,0 +1,65 @@ +import { mount } from 'enzyme'; +import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; +import { act } from 'react-dom/test-utils'; +import { getOffsetSizeFunc, getTabs, triggerResize } from './common/util'; + +describe('Tabs.Operation-Overflow', () => { + let domSpy: ReturnType; + let holder: HTMLDivElement; + + const hackOffsetInfo = { wrapper: 105, list: 110, add: 10, operation: 20 }; + + beforeAll(() => { + holder = document.createElement('div'); + document.body.appendChild(holder); + + function btnOffsetPosition() { + const btn = this as HTMLButtonElement; + const btnList = [...btn.parentNode.childNodes].filter(ele => + (ele as HTMLElement).className.includes('rc-tabs-tab'), + ); + const index = btnList.indexOf(btn); + return 20 * index; + } + + domSpy = spyElementPrototypes(HTMLElement, { + scrollIntoView: () => {}, + offsetWidth: { + get: getOffsetSizeFunc(hackOffsetInfo), + }, + offsetHeight: { + get: getOffsetSizeFunc(hackOffsetInfo), + }, + offsetLeft: { + get: btnOffsetPosition, + }, + offsetTop: { + get: btnOffsetPosition, + }, + }); + }); + + afterAll(() => { + domSpy.mockRestore(); + document.body.removeChild(holder); + }); + + it('should collapse', () => { + jest.useFakeTimers(); + const onEdit = jest.fn(); + const wrapper = mount(getTabs({ editable: { onEdit } })); + + triggerResize(wrapper); + act(() => { + jest.runAllTimers(); + wrapper.update(); + }); + expect( + wrapper.find('.rc-tabs-nav-operations').hasClass('rc-tabs-nav-operations-hidden'), + ).toBeFalsy(); + + wrapper.unmount(); + + jest.useRealTimers(); + }); +});