diff --git a/.dumi/hooks/useThemeAnimation.ts b/.dumi/hooks/useThemeAnimation.ts index de95e3789c..42ca520f2b 100644 --- a/.dumi/hooks/useThemeAnimation.ts +++ b/.dumi/hooks/useThemeAnimation.ts @@ -64,7 +64,6 @@ const useThemeAnimation = () => { event: React.MouseEvent, isDark: boolean, ) => { - // @ts-ignore if (!(event && typeof document.startViewTransition === 'function')) { return; } @@ -85,13 +84,10 @@ const useThemeAnimation = () => { 'color-scheme', ); document - // @ts-ignore .startViewTransition(async () => { // wait for theme change end while (colorBgElevated === animateRef.current.colorBgElevated) { - await new Promise((resolve) => { - setTimeout(resolve, 1000 / 60); - }); + await new Promise((resolve) => setTimeout(resolve, 1000 / 60)); } const root = document.documentElement; root.classList.remove(isDark ? 'dark' : 'light'); @@ -111,7 +107,6 @@ const useThemeAnimation = () => { // inject transition style useEffect(() => { - // @ts-ignore if (typeof document.startViewTransition === 'function') { updateCSS(viewTransitionStyle, 'view-transition-style'); } diff --git a/components/_util/convertToTooltipProps.ts b/components/_util/convertToTooltipProps.ts new file mode 100644 index 0000000000..29d8d7ca5e --- /dev/null +++ b/components/_util/convertToTooltipProps.ts @@ -0,0 +1,20 @@ +import type { ReactNode } from 'react'; +import { isValidElement } from 'react'; +import type { TooltipProps } from '../tooltip'; + +function convertToTooltipProps

(tooltip: P | ReactNode): P | null { + // isNil + if (tooltip === undefined || tooltip === null) { + return null; + } + + if (typeof tooltip === 'object' && !isValidElement(tooltip)) { + return tooltip as P; + } + + return { + title: tooltip, + } as P; +} + +export default convertToTooltipProps; diff --git a/components/float-button/FloatButton.tsx b/components/float-button/FloatButton.tsx index 26b4645c90..4a903116b1 100644 --- a/components/float-button/FloatButton.tsx +++ b/components/float-button/FloatButton.tsx @@ -1,7 +1,8 @@ -import React, { useContext, useMemo } from 'react'; +import React from 'react'; import omit from '@rc-component/util/lib/omit'; import classNames from 'classnames'; +import convertToTooltipProps from '../_util/convertToTooltipProps'; import { useZIndex } from '../_util/hooks/useZIndex'; import { devUseWarning } from '../_util/warning'; import Badge from '../badge'; @@ -13,12 +14,7 @@ import type BackTop from './BackTop'; import FloatButtonGroupContext from './context'; import Content from './FloatButtonContent'; import type FloatButtonGroup from './FloatButtonGroup'; -import type { - FloatButtonContentProps, - FloatButtonElement, - FloatButtonProps, - FloatButtonShape, -} from './interface'; +import type { FloatButtonElement, FloatButtonProps, FloatButtonShape } from './interface'; import type PurePanel from './PurePanel'; import useStyle from './style'; @@ -39,8 +35,8 @@ const InternalFloatButton = React.forwardRef(ConfigContext); - const groupShape = useContext(FloatButtonGroupContext); + const { getPrefixCls, direction } = React.useContext(ConfigContext); + const groupShape = React.useContext(FloatButtonGroupContext); const prefixCls = getPrefixCls(floatButtonPrefixCls, customizePrefixCls); const rootCls = useCSSVarCls(prefixCls); const [hashId, cssVarCls] = useStyle(prefixCls, rootCls); @@ -69,14 +65,9 @@ const InternalFloatButton = React.forwardRef( - () => ({ prefixCls, description, icon, type }), - [prefixCls, description, icon, type], - ); - let buttonNode = (

- +
); @@ -84,12 +75,10 @@ const InternalFloatButton = React.forwardRef{buttonNode}; } - if ('tooltip' in props) { - buttonNode = ( - - {buttonNode} - - ); + // ============================ Tooltip ============================ + const tooltipProps = convertToTooltipProps(tooltip); + if (tooltipProps) { + buttonNode = {buttonNode}; } if (process.env.NODE_ENV !== 'production') { @@ -102,16 +91,14 @@ const InternalFloatButton = React.forwardRef - {buttonNode} - - ) : ( - - ) + return props.href ? ( + + {buttonNode} + + ) : ( + ); }); diff --git a/components/float-button/FloatButtonContent.tsx b/components/float-button/FloatButtonContent.tsx index 3e388c84e9..671e216e88 100644 --- a/components/float-button/FloatButtonContent.tsx +++ b/components/float-button/FloatButtonContent.tsx @@ -5,20 +5,16 @@ import classNames from 'classnames'; import type { FloatButtonContentProps } from './interface'; const FloatButtonContent: React.FC = (props) => { - const { icon, description, prefixCls, className } = props; + const { icon, description, prefixCls, className, ...rest } = props; + const defaultElement = (
); + return ( -
+
{icon || description ? ( <> {icon &&
{icon}
} diff --git a/components/float-button/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/float-button/__tests__/__snapshots__/demo-extend.test.ts.snap index 54359fa078..39a93b3285 100644 --- a/components/float-button/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/float-button/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -140,12 +140,12 @@ Array [
-
- - - -
-
-
-
-
-
-
-
- +
+
+
+ +
+
+ , + , +] `; exports[`renders components/float-button/demo/tooltip.tsx extend context correctly 2`] = `[]`; diff --git a/components/float-button/__tests__/__snapshots__/demo.test.ts.snap b/components/float-button/__tests__/__snapshots__/demo.test.ts.snap index fbb3088aba..2a5eb879ed 100644 --- a/components/float-button/__tests__/__snapshots__/demo.test.ts.snap +++ b/components/float-button/__tests__/__snapshots__/demo.test.ts.snap @@ -1974,43 +1974,83 @@ Array [ `; exports[`renders components/float-button/demo/tooltip.tsx correctly 1`] = ` -
- + , + , +] `; exports[`renders components/float-button/demo/type.tsx correctly 1`] = ` diff --git a/components/float-button/__tests__/image.test.ts b/components/float-button/__tests__/image.test.ts index 7b5d460db7..69e2656647 100644 --- a/components/float-button/__tests__/image.test.ts +++ b/components/float-button/__tests__/image.test.ts @@ -1,5 +1,5 @@ import { imageDemoTest } from '../../../tests/shared/imageTest'; describe('float-button image', () => { - imageDemoTest('float-button', { onlyViewport: ['back-top.tsx'] }); + imageDemoTest('float-button', { onlyViewport: true }); }); diff --git a/components/float-button/__tests__/index.test.tsx b/components/float-button/__tests__/index.test.tsx index 5148c24d28..590e2461b7 100644 --- a/components/float-button/__tests__/index.test.tsx +++ b/components/float-button/__tests__/index.test.tsx @@ -63,15 +63,27 @@ describe('FloatButton', () => { errSpy.mockRestore(); }); - it('tooltip should support number `0`', async () => { - jest.useFakeTimers(); - const { container } = render(); - fireEvent.mouseEnter(container.querySelector('.ant-float-btn-body')!); - await waitFakeTimer(); - const element = container.querySelector('.ant-tooltip')?.querySelector('.ant-tooltip-inner'); - expect(element?.textContent).toBe('0'); - jest.clearAllTimers(); - jest.useRealTimers(); + describe('tooltip', () => { + it('tooltip should support number `0`', async () => { + jest.useFakeTimers(); + const { container } = render(); + fireEvent.mouseEnter(container.querySelector('.ant-float-btn-body')!); + await waitFakeTimer(); + const element = container.querySelector('.ant-tooltip')?.querySelector('.ant-tooltip-inner'); + expect(element?.textContent).toBe('0'); + jest.clearAllTimers(); + jest.useRealTimers(); + }); + it('tooltip should support tooltipProps', async () => { + jest.useFakeTimers(); + const { container } = render(); + fireEvent.mouseEnter(container.querySelector('.ant-float-btn-body')!); + await waitFakeTimer(); + const element = container.querySelector('.ant-tooltip')?.querySelector('.ant-tooltip-inner'); + expect(element?.textContent).toBe('hi'); + jest.clearAllTimers(); + jest.useRealTimers(); + }); }); it('getOffset should return 0 when radius is 0', () => { diff --git a/components/float-button/demo/tooltip.tsx b/components/float-button/demo/tooltip.tsx index 73574da13b..6484e55037 100644 --- a/components/float-button/demo/tooltip.tsx +++ b/components/float-button/demo/tooltip.tsx @@ -1,6 +1,19 @@ import React from 'react'; import { FloatButton } from 'antd'; -const App: React.FC = () => Documents
} />; +const App: React.FC = () => ( + <> + + Documents
} /> + +); export default App; diff --git a/components/float-button/index.en-US.md b/components/float-button/index.en-US.md index eb7790c592..3698ae0019 100644 --- a/components/float-button/index.en-US.md +++ b/components/float-button/index.en-US.md @@ -44,7 +44,7 @@ Common props ref:[Common props](/docs/react/common-props) | --- | --- | --- | --- | --- | | icon | Set the icon component of button | ReactNode | - | | | description | Text and other | ReactNode | - | | -| tooltip | The text shown in the tooltip | ReactNode \| () => ReactNode | | | +| tooltip | The text shown in the tooltip | ReactNode \| [TooltipProps](/components/tooltip#api) | - | TooltipProps: 5.25.0 | | type | Setting button type | `default` \| `primary` | `default` | | | shape | Setting button shape | `circle` \| `square` | `circle` | | | onClick | Set the handler to handle `click` event | (event) => void | - | | diff --git a/components/float-button/index.zh-CN.md b/components/float-button/index.zh-CN.md index cf58b84b17..069226bbdc 100644 --- a/components/float-button/index.zh-CN.md +++ b/components/float-button/index.zh-CN.md @@ -45,7 +45,7 @@ tag: 5.0.0 | --- | --- | --- | --- | --- | | icon | 自定义图标 | ReactNode | - | | | description | 文字及其它内容 | ReactNode | - | | -| tooltip | 气泡卡片的内容 | ReactNode \| () => ReactNode | - | | +| tooltip | 气泡卡片的内容 | ReactNode \| [TooltipProps](/components/tooltip-cn#api) | - | TooltipProps: 5.25.0 | | type | 设置按钮类型 | `default` \| `primary` | `default` | | | shape | 设置按钮形状 | `circle` \| `square` | `circle` | | | onClick | 点击按钮时的回调 | (event) => void | - | | diff --git a/components/float-button/interface.ts b/components/float-button/interface.ts index bc5960e6a2..83c62bcd36 100644 --- a/components/float-button/interface.ts +++ b/components/float-button/interface.ts @@ -27,7 +27,7 @@ export interface FloatButtonProps extends React.DOMAttributes, ...restTooltipProps } = tooltipProps; diff --git a/components/input/TextArea.tsx b/components/input/TextArea.tsx index 3b736faf34..a92172676a 100644 --- a/components/input/TextArea.tsx +++ b/components/input/TextArea.tsx @@ -20,6 +20,7 @@ import type { SizeType } from '../config-provider/SizeContext'; import { FormItemInputContext } from '../form/context'; import useVariant from '../form/hooks/useVariants'; import { useCompactItemContext } from '../space/Compact'; +import useHandleResizeWrapper from './hooks/useHandleResizeWrapper'; import type { InputFocusOptions } from './Input'; import { triggerFocus } from './Input'; import { useSharedStyle } from './style'; @@ -58,6 +59,7 @@ const TextArea = forwardRef((props, ref) => { style, styles, variant: customVariant, + showCount, ...rest } = props; @@ -117,6 +119,8 @@ const TextArea = forwardRef((props, ref) => { const mergedAllowClear = getAllowClear(allowClear ?? contextAllowClear); + const { handleResizeWrapper } = useHandleResizeWrapper(); + return ( ((props, ref) => { prefixCls={prefixCls} suffix={hasFeedback && {feedbackIcon}} ref={innerRef} + onResize={(size) => { + rest.onResize?.(size); + showCount && handleResizeWrapper(innerRef.current); + }} /> ); }); diff --git a/components/input/__tests__/textarea.test.tsx b/components/input/__tests__/textarea.test.tsx index b026a2b652..fe34e8b16a 100644 --- a/components/input/__tests__/textarea.test.tsx +++ b/components/input/__tests__/textarea.test.tsx @@ -9,11 +9,14 @@ import { fireEvent, pureRender, render, + renderHook, triggerResize, waitFakeTimer, waitFakeTimer19, } from '../../../tests/utils'; import type { TextAreaRef } from '../TextArea'; +import useHandleResizeWrapper from '../hooks/useHandleResizeWrapper'; +import type { TextAreaRef as RcTextAreaRef } from 'rc-textarea'; const { TextArea } = Input; @@ -531,3 +534,112 @@ describe('TextArea allowClear', () => { errSpy.mockRestore(); }); }); + +describe('TextArea useHandleResizeWrapper', () => { + let requestAnimationFrameSpy: jest.SpyInstance; + + beforeAll(() => { + // Use fake timers to control requestAnimationFrame. + jest.useFakeTimers(); + // Override requestAnimationFrame to simulate a 16ms delay. + requestAnimationFrameSpy = jest + .spyOn(window, 'requestAnimationFrame') + .mockImplementation((cb: FrameRequestCallback) => { + return window.setTimeout(() => cb(performance.now()), 16); + }); + }); + + afterAll(() => { + jest.useRealTimers(); + requestAnimationFrameSpy.mockRestore(); + }); + + it('does nothing when rcTextArea is null', () => { + const { result } = renderHook(() => useHandleResizeWrapper()); + // Calling with null should not throw or change anything. + expect(() => result.current?.handleResizeWrapper(null)).not.toThrow(); + }); + + it('does nothing when style width does not include "px"', () => { + const { result } = renderHook(() => useHandleResizeWrapper()); + + const fakeRcTextArea = { + resizableTextArea: { + textArea: { + style: { + width: '100', // missing 'px' + }, + }, + }, + nativeElement: { + offsetWidth: 110, + style: {} as any, + }, + } as unknown as RcTextAreaRef; + + result.current?.handleResizeWrapper(fakeRcTextArea); + + // Fast-forward time to see if any scheduled callback would execute. + jest.advanceTimersByTime(16); + // nativeElement.style.width remains unchanged. + expect(fakeRcTextArea.nativeElement.style.width).toBeUndefined(); + }); + + it('adjusts width correctly when offsetWidth is slightly greater than the textArea width (increased scenario)', () => { + const { result } = renderHook(() => useHandleResizeWrapper()); + + const fakeRcTextArea = { + resizableTextArea: { + textArea: { + style: { + width: '100px', // valid width with px + }, + }, + }, + // offsetWidth is 101 so the difference is 1 (< ELEMENT_GAP of 2) + nativeElement: { + offsetWidth: 101, + style: {} as any, + }, + } as unknown as RcTextAreaRef; + + // Immediately after calling handleResizeWrapper, the update is scheduled. + expect(fakeRcTextArea.nativeElement.style.width).toBeUndefined(); + + result.current?.handleResizeWrapper(fakeRcTextArea); + + // Fast-forward time to trigger the requestAnimationFrame callback. + jest.advanceTimersByTime(16); + // Expected new width: 100 + 2 = 102px. + expect(fakeRcTextArea.nativeElement.style.width).toBe('102px'); + }); + + it('adjusts width correctly when offsetWidth is significantly greater than the textArea width (decreased scenario)', () => { + const { result } = renderHook(() => useHandleResizeWrapper()); + + const fakeRcTextArea = { + resizableTextArea: { + textArea: { + style: { + width: '100px', + }, + }, + }, + // offsetWidth is 105 so the difference is 5 (> ELEMENT_GAP of 2) + nativeElement: { + offsetWidth: 105, + style: {} as any, + }, + } as unknown as RcTextAreaRef; + + // Immediately after calling handleResizeWrapper, the update is scheduled. + expect(fakeRcTextArea.nativeElement.style.width).toBeUndefined(); + + result.current?.handleResizeWrapper(fakeRcTextArea); + + // Fast-forward time to trigger the requestAnimationFrame callback. + jest.advanceTimersByTime(16); + // Expected new width remains: 100 + 2 = 102px. + expect(fakeRcTextArea.nativeElement.style.width).toBe('102px'); + }); +}); diff --git a/components/input/hooks/useHandleResizeWrapper.ts b/components/input/hooks/useHandleResizeWrapper.ts new file mode 100644 index 0000000000..5942559581 --- /dev/null +++ b/components/input/hooks/useHandleResizeWrapper.ts @@ -0,0 +1,40 @@ +import { TextAreaRef } from 'rc-textarea'; +import React from 'react'; + +type ResizeWrapperHandler = (rcTextArea: TextAreaRef | null) => void; + +const ELEMENT_GAP = 2; + +const adjustElementWidth = (width: number, wrapper: HTMLElement): void => { + if (wrapper.offsetWidth - width < ELEMENT_GAP) { + // The textarea's width is increased + wrapper.style.width = `${width + ELEMENT_GAP}px`; + } else if (wrapper.offsetWidth - width > ELEMENT_GAP) { + // The textarea's width is decreased + wrapper.style.width = `${width + ELEMENT_GAP}px`; + } +}; + +let isScheduled = false; +const requestAnimationFrameDecorator = (callback: () => void) => { + if (!isScheduled) { + isScheduled = true; + + requestAnimationFrame(() => { + callback(); + isScheduled = false; + }); + } +}; + +export default function useHandleResizeWrapper(): { handleResizeWrapper: ResizeWrapperHandler } { + const handleResizeWrapper: ResizeWrapperHandler = React.useCallback((rcTextArea) => { + if (!rcTextArea) return; + if (rcTextArea.resizableTextArea.textArea.style.width.includes('px')) { + const width = parseInt(rcTextArea.resizableTextArea.textArea.style.width.replace('px', '')); + requestAnimationFrameDecorator(() => adjustElementWidth(width, rcTextArea.nativeElement)); + } + }, []); + + return { handleResizeWrapper }; +} diff --git a/components/upload/__tests__/requests.ts b/components/upload/__tests__/requests.ts index b0d2e42b0e..21935f4d80 100644 --- a/components/upload/__tests__/requests.ts +++ b/components/upload/__tests__/requests.ts @@ -2,14 +2,12 @@ import type { UploadProps } from '../interface'; export const successRequest: UploadProps['customRequest'] = ({ onSuccess, file }) => { setTimeout(() => { - // @ts-ignore onSuccess?.(null, file); }); }; export const errorRequest: UploadProps['customRequest'] = ({ onError }) => { setTimeout(() => { - // @ts-ignore - onError?.(); + onError?.(new Error('test error')); }); }; diff --git a/contributors.json b/contributors.json index a991ed7cb7..a2b3dfdfb5 100644 --- a/contributors.json +++ b/contributors.json @@ -89,6 +89,7 @@ "Artin", "Arvin Xu", "Ash Kumar", + "Ashcon Partovi", "Ashot Mnatsakanyan", "Austaras", "Avan", @@ -198,6 +199,7 @@ "Daewoong Moon", "Dalton Craven", "Damian Green", + "Dan M.", "Dan Minshew", "Dana Janoskova", "Dane David", @@ -349,6 +351,7 @@ "HJin.me", "Hai Phan Nguyen", "Haibin Yu", + "Hakan Kosdag", "Hale Deng", "Han Han", "Hanai", @@ -383,6 +386,7 @@ "Humble", "Hyunseok.Kim", "ILdar Nogmanov", + "Ideveloper (이승규)", "Igor", "Igor Andriushchenko", "Igor G", @@ -699,6 +703,7 @@ "Muhammad Abuzar", "Muhammad Sameer", "Muhammad Sohaib Raza", + "Muhammad Taif Khan", "MuxinFeng", "MyeongHyun Lew", "Mykyta Velykanov", @@ -987,11 +992,13 @@ "Vu Hoang Minh", "Vyacheslav Kamenev", "Vyacheslav Sedykh", + "Waiter", "Walter Barbagallo", "Wang Jun", "Wang Riwu", "Wang Zhengchen", "Wang yb", + "Wangye", "Wanpan", "Warren Seymour", "WeLong", @@ -1121,6 +1128,7 @@ "appleshell", "arange", "arifemrecelik", + "arron.laihongwei", "arturpfb", "ascodelife", "ascoders", @@ -1398,6 +1406,7 @@ "netcon", "neverland", "ngolin", + "nicholas-codecov", "nick-ChenZe", "niko", "nitinknolder", @@ -1594,6 +1603,7 @@ "zhoulixiang", "zhuguibiao", "zhujun24", + "zhuzhu_coder", "zhyupe", "zilong", "zinkey", @@ -1607,12 +1617,12 @@ "ztplz", "zty", "zuiidea", - "zx6658", "zxyao", "zytjs", "zz", "°))))彡", "Ömer Faruk APLAK", + "Đào Văn Hùng", "Ștefan Filip", "Зухриддин Камильжанов", "रोहन मल्होत्रा", @@ -1644,6 +1654,7 @@ "只捱宅", "可乐", "叶枫", + "合木", "吕立青", "吴泽康", "啸生",