From da3d43c7a0aa0c7396d2ce196a5c4dfa7c20ea96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Mon, 16 Oct 2023 20:27:02 +0800 Subject: [PATCH] fix: Offset logic with decimal (#676) * fix: blink of decimal * test: add test case * chore: all width --- src/TabNavList/index.tsx | 95 ++++++++++++++++++++++++++-------------- tests/common/util.tsx | 14 ++++-- tests/overflow.test.tsx | 75 ++++++++++++++++++++++++++++++- 3 files changed, 147 insertions(+), 37 deletions(-) diff --git a/src/TabNavList/index.tsx b/src/TabNavList/index.tsx index 247c01ff..19284730 100644 --- a/src/TabNavList/index.tsx +++ b/src/TabNavList/index.tsx @@ -4,6 +4,8 @@ import useEvent from 'rc-util/lib/hooks/useEvent'; import { useComposeRef } from 'rc-util/lib/ref'; import * as React from 'react'; import { useEffect, useRef, useState } from 'react'; +import type { GetIndicatorSize } from '../hooks/useIndicator'; +import useIndicator from '../hooks/useIndicator'; import useOffsets from '../hooks/useOffsets'; import useSyncState from '../hooks/useSyncState'; import useTouchMove from '../hooks/useTouchMove'; @@ -26,8 +28,6 @@ import AddButton from './AddButton'; import ExtraContent from './ExtraContent'; import OperationNode from './OperationNode'; import TabNode from './TabNode'; -import useIndicator from '../hooks/useIndicator'; -import type { GetIndicatorSize } from '../hooks/useIndicator'; export interface TabNavListProps { id: string; @@ -53,8 +53,31 @@ export interface TabNavListProps { indicatorSize?: GetIndicatorSize; } +const getTabSize = (tab: HTMLElement, containerRect: { x: number; y: number }) => { + // tabListRef + const { offsetWidth, offsetHeight, offsetTop, offsetLeft } = tab; + const { width, height, x, y } = tab.getBoundingClientRect(); + + // Use getBoundingClientRect to avoid decimal inaccuracy + if (Math.abs(width - offsetWidth) < 1) { + return [width, height, x - containerRect.x, y - containerRect.y]; + } + + return [offsetWidth, offsetHeight, offsetLeft, offsetTop]; +}; + const getSize = (refObj: React.RefObject): SizeInfo => { const { offsetWidth = 0, offsetHeight = 0 } = refObj.current || {}; + + // Use getBoundingClientRect to avoid decimal inaccuracy + if (refObj.current) { + const { width, height } = refObj.current.getBoundingClientRect(); + + if (Math.abs(width - offsetWidth) < 1) { + return [width, height]; + } + } + return [offsetWidth, offsetHeight]; }; @@ -313,14 +336,20 @@ function TabNavList(props: TabNavListProps, ref: React.Ref) { const updateTabSizes = () => setTabSizes(() => { const newSizes: TabSizeMap = new Map(); + const listRect = tabListRef.current?.getBoundingClientRect(); + tabs.forEach(({ key }) => { - const btnNode = tabListRef.current?.querySelector(`[data-node-key="${genDataNodeKey(key)}"]`); + const btnNode = tabListRef.current?.querySelector( + `[data-node-key="${genDataNodeKey(key)}"]`, + ); if (btnNode) { + const [width, height, left, top] = getTabSize(btnNode, listRect); + newSizes.set(key, { - width: btnNode.offsetWidth, - height: btnNode.offsetHeight, - left: btnNode.offsetLeft, - top: btnNode.offsetTop, + width, + height, + left, + top, }); } }); @@ -370,7 +399,7 @@ function TabNavList(props: TabNavListProps, ref: React.Ref) { horizontal: tabPositionTopOrBottom, rtl, indicatorSize, - }) + }); // ========================= Effect ======================== useEffect(() => { @@ -437,33 +466,33 @@ function TabNavList(props: TabNavListProps, ref: React.Ref) { ref={tabsWrapperRef} > -
- {tabNodes} - - -
-
+ > + {tabNodes} + + +
+
diff --git a/tests/common/util.tsx b/tests/common/util.tsx index 02ae5f61..b19cd35d 100644 --- a/tests/common/util.tsx +++ b/tests/common/util.tsx @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-invalid-this */ import { act } from '@testing-library/react'; -import type { ReactWrapper } from 'enzyme'; import { _rs as onEsResize } from 'rc-resize-observer/es/utils/observerUtil'; import { _rs as onLibResize } from 'rc-resize-observer/lib/utils/observerUtil'; import React from 'react'; @@ -17,6 +16,7 @@ import type { TabsProps } from '../../src/Tabs'; export interface HackInfo { container?: number; tabNode?: number; + tabNodeList?: number; add?: number; more?: number; extra?: number; @@ -25,7 +25,15 @@ export interface HackInfo { export function getOffsetSizeFunc(info: HackInfo = {}) { return function getOffsetSize() { - const { container = 50, extra = 10, tabNode = 20, add = 10, more = 10, dropdown = 10 } = info; + const { + container = 50, + extra = 10, + tabNodeList, + tabNode = 20, + add = 10, + more = 10, + dropdown = 10, + } = info; if (this.classList.contains('rc-tabs-nav')) { return container; @@ -36,7 +44,7 @@ export function getOffsetSizeFunc(info: HackInfo = {}) { } if (this.classList.contains('rc-tabs-nav-list')) { - return this.querySelectorAll('.rc-tabs-tab').length * tabNode + add; + return tabNodeList || this.querySelectorAll('.rc-tabs-tab').length * tabNode + add; } if (this.classList.contains('rc-tabs-tab')) { diff --git a/tests/overflow.test.tsx b/tests/overflow.test.tsx index 718e4ae6..eba7dccb 100644 --- a/tests/overflow.test.tsx +++ b/tests/overflow.test.tsx @@ -11,7 +11,7 @@ import { getTabs, getTransformX, getTransformY, - triggerResize, waitFakeTimer, + triggerResize, } from './common/util'; describe('Tabs.Overflow', () => { @@ -19,7 +19,13 @@ describe('Tabs.Overflow', () => { const hackOffsetInfo: HackInfo = {}; + let mockGetBoundingClientRect: ( + ele: HTMLElement, + ) => { x: number; y: number; width: number; height: number } | void = null; + beforeEach(() => { + mockGetBoundingClientRect = null; + Object.keys(hackOffsetInfo).forEach(key => { delete hackOffsetInfo[key]; }); @@ -40,6 +46,16 @@ describe('Tabs.Overflow', () => { offsetTop: { get: btnOffsetPosition, }, + getBoundingClientRect() { + return ( + mockGetBoundingClientRect?.(this) || { + x: 0, + y: 0, + width: 0, + height: 0, + } + ); + }, }); }); @@ -483,4 +499,61 @@ describe('Tabs.Overflow', () => { }); expect(document.querySelector('.rc-tabs-dropdown')).toHaveClass('custom-popup'); }); + + it('correct handle decimal', () => { + hackOffsetInfo.container = 29; + hackOffsetInfo.tabNodeList = 29; + hackOffsetInfo.tabNode = 15; + + mockGetBoundingClientRect = ele => { + if (ele.classList.contains('rc-tabs-tab')) { + const sharedRect = { + x: 0, + y: 0, + width: 14.5, + height: 14.5, + }; + + return ele.getAttribute('data-node-key') === 'bamboo' + ? { + ...sharedRect, + } + : { + ...sharedRect, + x: 14.5, + }; + } + // console.log('ele!!!', ele.className); + }; + + jest.useFakeTimers(); + const { container } = render( + getTabs({ + defaultActiveKey: 'little', + items: [ + { + label: 'bamboo', + key: 'bamboo', + children: 'Bamboo', + }, + { + label: 'little', + key: 'little', + children: 'Little', + }, + ], + }), + ); + + act(() => { + jest.runAllTimers(); + }); + + expect(container.querySelector('.rc-tabs-nav-operations-hidden')).toBeTruthy(); + expect(container.querySelector('.rc-tabs-ink-bar')).toHaveStyle({ + left: '21.75px', + }); + + jest.useRealTimers(); + }); });