From dece1b005c9ef77df82ed4247d918e9b3e818711 Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Mon, 24 Nov 2025 22:59:47 +0800 Subject: [PATCH 01/12] chore: add @testing-library/jest-dom as a dev dependency and update tsconfig types --- package.json | 1 + tsconfig.json | 1 + 2 files changed, 2 insertions(+) diff --git a/package.json b/package.json index 8fe32ce..c4c02e7 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "devDependencies": { "@rc-component/father-plugin": "^2.0.1", "@rc-component/np": "^1.0.3", + "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@types/jest": "^29.4.0", "@types/node": "^22.15.18", diff --git a/tsconfig.json b/tsconfig.json index 33bd1ce..f950160 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "declaration": true, "skipLibCheck": true, "esModuleInterop": true, + "types": ["@testing-library/jest-dom"], "paths": { "@/*": [ "src/*" From b7388d45bb40fca82babd06144504417073bee27 Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Tue, 25 Nov 2025 01:26:30 +0800 Subject: [PATCH 02/12] feat: improve a11y --- src/Tooltip.tsx | 53 +++++++++++++++++++++++++++++++++++++++--- tests/index.test.tsx | 55 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 102 insertions(+), 6 deletions(-) diff --git a/src/Tooltip.tsx b/src/Tooltip.tsx index 4808238..c08aa2e 100644 --- a/src/Tooltip.tsx +++ b/src/Tooltip.tsx @@ -60,7 +60,7 @@ export interface TooltipRef extends TriggerRef {} const Tooltip = React.forwardRef((props, ref) => { const { - trigger = ['hover'], + trigger = ['hover','focus'], mouseEnterDelay = 0, mouseLeaveDelay = 0.1, prefixCls = 'rc-tooltip', @@ -79,6 +79,7 @@ const Tooltip = React.forwardRef((props, ref) => { showArrow = true, classNames, styles, + forceRender, ...restProps } = props; @@ -93,6 +94,51 @@ const Tooltip = React.forwardRef((props, ref) => { extraProps.popupVisible = props.visible; } + const isControlled = 'visible' in props; + const mergedVisible = props.visible; + const [popupMounted, setPopupMounted] = React.useState(() => { + if (forceRender) { + return true; + } + if (isControlled) { + return mergedVisible; + } + return defaultVisible; + }); + + const updatePopupMounted = React.useCallback( + (nextVisible: boolean) => { + setPopupMounted((prev) => { + if (nextVisible) { + return true; + } + + if (destroyOnHidden) { + return false; + } + + return prev; + }); + }, + [forceRender, destroyOnHidden], + ); + + const handleVisibleChange = (nextVisible: boolean) => { + updatePopupMounted(nextVisible); + onVisibleChange?.(nextVisible); + }; + + React.useEffect(() => { + if (forceRender) { + setPopupMounted(true); + return; + } + + if (isControlled) { + setPopupMounted(mergedVisible); + } + }, [forceRender, isControlled, mergedVisible]); + // ========================= Arrow ========================== // Process arrow configuration const mergedArrow = React.useMemo(() => { @@ -118,7 +164,7 @@ const Tooltip = React.forwardRef((props, ref) => { const originalProps = child?.props || {}; const childProps = { ...originalProps, - 'aria-describedby': overlay ? mergedId : null, + 'aria-describedby': overlay && popupMounted ? mergedId : null, }; return React.cloneElement(children, childProps) as any; }; @@ -145,10 +191,11 @@ const Tooltip = React.forwardRef((props, ref) => { ref={triggerRef} popupAlign={align} getPopupContainer={getTooltipContainer} - onOpenChange={onVisibleChange} + onOpenChange={handleVisibleChange} afterOpenChange={afterVisibleChange} popupMotion={motion} defaultPopupVisible={defaultVisible} + forceRender={forceRender} autoDestroy={destroyOnHidden} mouseLeaveDelay={mouseLeaveDelay} popupStyle={styles?.root} diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 37781e8..4892d7a 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -502,14 +502,24 @@ describe('rc-tooltip', () => { }); describe('children handling', () => { - it('should pass aria-describedby to child element when overlay exists', () => { + it('should only set aria-describedby once popup is mounted', async () => { const { container } = render( - + , ); - expect(container.querySelector('button')).toHaveAttribute('aria-describedby', 'test-id'); + const btn = container.querySelector('button'); + expect(btn).not.toHaveAttribute('aria-describedby'); + + fireEvent.click(btn); + await waitFakeTimers(); + const describedby = btn.getAttribute('aria-describedby'); + expect(describedby).toBeTruthy(); + + fireEvent.click(btn); + await waitFakeTimers(); + expect(btn).toHaveAttribute('aria-describedby', describedby); }); it('should not pass aria-describedby when overlay is empty', () => { @@ -522,6 +532,45 @@ describe('rc-tooltip', () => { expect(container.querySelector('button')).not.toHaveAttribute('aria-describedby'); }); + it('should set aria-describedby immediately when defaultVisible is true', () => { + const { container } = render( + + + , + ); + + expect(container.querySelector('button')).toHaveAttribute('aria-describedby'); + }); + + it('should set aria-describedby immediately when forceRender is true', () => { + const { container } = render( + + + , + ); + + expect(container.querySelector('button')).toHaveAttribute('aria-describedby'); + }); + + it('should remove aria-describedby when popup is destroyed on hide', async () => { + const { container } = render( + + + , + ); + + const btn = container.querySelector('button'); + expect(btn).not.toHaveAttribute('aria-describedby'); + + fireEvent.click(btn); + await waitFakeTimers(); + expect(btn).toHaveAttribute('aria-describedby'); + + fireEvent.click(btn); + await waitFakeTimers(); + expect(btn).not.toHaveAttribute('aria-describedby'); + }); + it('should preserve original props of children', () => { const onMouseEnter = jest.fn(); From 2a4f3300e9d7ebdbfbaeba9eb24752b95e5559d7 Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Tue, 25 Nov 2025 10:08:27 +0800 Subject: [PATCH 03/12] fix test --- src/Tooltip.tsx | 4 ++-- tests/index.test.tsx | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Tooltip.tsx b/src/Tooltip.tsx index c08aa2e..6f2abfd 100644 --- a/src/Tooltip.tsx +++ b/src/Tooltip.tsx @@ -135,9 +135,9 @@ const Tooltip = React.forwardRef((props, ref) => { } if (isControlled) { - setPopupMounted(mergedVisible); + updatePopupMounted(mergedVisible); } - }, [forceRender, isControlled, mergedVisible]); + }, [forceRender, isControlled, mergedVisible, updatePopupMounted]); // ========================= Arrow ========================== // Process arrow configuration diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 4892d7a..342d5ad 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -552,6 +552,25 @@ describe('rc-tooltip', () => { expect(container.querySelector('button')).toHaveAttribute('aria-describedby'); }); + it('should keep aria-describedby when controlled hidden without destroy', () => { + const overlay = 'tooltip content'; + const { container, rerender } = render( + + + , + ); + + expect(container.querySelector('button')).toHaveAttribute('aria-describedby'); + + rerender( + + + , + ); + + expect(container.querySelector('button')).toHaveAttribute('aria-describedby'); + }); + it('should remove aria-describedby when popup is destroyed on hide', async () => { const { container } = render( From 625204d546e021dd9aaf36b3145529a381ad5757 Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Tue, 25 Nov 2025 10:12:13 +0800 Subject: [PATCH 04/12] update --- src/Tooltip.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Tooltip.tsx b/src/Tooltip.tsx index 6f2abfd..e5ac3d0 100644 --- a/src/Tooltip.tsx +++ b/src/Tooltip.tsx @@ -4,7 +4,7 @@ import type { ActionType, AlignType } from '@rc-component/trigger/lib/interface' import useId from '@rc-component/util/lib/hooks/useId'; import { clsx } from 'clsx'; import * as React from 'react'; -import { useImperativeHandle, useRef } from 'react'; +import { useImperativeHandle, useRef, useEffect, useCallback } from 'react'; import { placements } from './placements'; import Popup from './Popup'; @@ -60,7 +60,7 @@ export interface TooltipRef extends TriggerRef {} const Tooltip = React.forwardRef((props, ref) => { const { - trigger = ['hover','focus'], + trigger = ['hover', 'focus'], mouseEnterDelay = 0, mouseLeaveDelay = 0.1, prefixCls = 'rc-tooltip', @@ -106,7 +106,7 @@ const Tooltip = React.forwardRef((props, ref) => { return defaultVisible; }); - const updatePopupMounted = React.useCallback( + const updatePopupMounted = useCallback( (nextVisible: boolean) => { setPopupMounted((prev) => { if (nextVisible) { @@ -120,7 +120,7 @@ const Tooltip = React.forwardRef((props, ref) => { return prev; }); }, - [forceRender, destroyOnHidden], + [destroyOnHidden], ); const handleVisibleChange = (nextVisible: boolean) => { @@ -128,7 +128,7 @@ const Tooltip = React.forwardRef((props, ref) => { onVisibleChange?.(nextVisible); }; - React.useEffect(() => { + useEffect(() => { if (forceRender) { setPopupMounted(true); return; From e081d1b46770e08c09428894ff2d66b2975602fe Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Tue, 25 Nov 2025 12:06:16 +0800 Subject: [PATCH 05/12] update --- README.md | 34 +++++++++++++++++----------------- src/Tooltip.tsx | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index c2890fb..43f84cf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# rc-tooltip +# @rc-component/tooltip React Tooltip @@ -9,18 +9,18 @@ React Tooltip [![bundle size][bundlephobia-image]][bundlephobia-url] [![dumi][dumi-image]][dumi-url] -[npm-image]: http://img.shields.io/npm/v/rc-tooltip.svg?style=flat-square -[npm-url]: http://npmjs.org/package/rc-tooltip -[travis-image]: https://img.shields.io/travis/react-component/tooltip/master?style=flat-square -[travis-url]: https://travis-ci.com/react-component/tooltip -[github-actions-image]: https://github.com/react-component/tooltip/actions/workflows/react-component-ci.yml/badge.svg -[github-actions-url]: https://github.com/react-component/tooltip/actions/workflows/react-component-ci.yml -[codecov-image]: https://img.shields.io/codecov/c/github/react-component/tooltip/master.svg?style=flat-square -[codecov-url]: https://app.codecov.io/gh/react-component/tooltip -[download-image]: https://img.shields.io/npm/dm/rc-tooltip.svg?style=flat-square -[download-url]: https://npmjs.org/package/rc-tooltip -[bundlephobia-url]: https://bundlephobia.com/package/rc-tooltip -[bundlephobia-image]: https://badgen.net/bundlephobia/minzip/rc-tooltip +[npm-image]: http://img.shields.io/npm/v/@rc-component/tooltip.svg?style=flat-square +[npm-url]: http://npmjs.org/package/@rc-component/tooltip +[travis-image]: https://img.shields.io/travis/@rc-component/tooltip/master?style=flat-square +[travis-url]: https://travis-ci.com/@rc-component/tooltip +[github-actions-image]: https://github.com/@rc-component/tooltip/actions/workflows/react-component-ci.yml/badge.svg +[github-actions-url]: https://github.com/@rc-component/tooltip/actions/workflows/react-component-ci.yml +[codecov-image]: https://img.shields.io/codecov/c/github/@rc-component/tooltip/master.svg?style=flat-square +[codecov-url]: https://app.codecov.io/gh/@rc-component/tooltip +[download-image]: https://img.shields.io/npm/dm/@rc-component/tooltip.svg?style=flat-square +[download-url]: https://npmjs.org/package/@rc-component/tooltip +[bundlephobia-url]: https://bundlephobia.com/package/@rc-component/tooltip +[bundlephobia-image]: https://badgen.net/bundlephobia/minzip/@rc-component/tooltip [dumi-url]: https://github.com/umijs/dumi [dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square @@ -36,18 +36,18 @@ React Tooltip ## Install -[![rc-tooltip](https://nodei.co/npm/rc-tooltip.png)](https://npmjs.org/package/rc-tooltip) +[![@rc-component/tooltip](https://nodei.co/npm/@rc-component/tooltip.png)](https://npmjs.org/package/@rc-component/tooltip) ## Usage ```js -var Tooltip = require('rc-tooltip'); +var Tooltip = require('@rc-component/tooltip'); var React = require('react'); var ReactDOM = require('react-dom'); // By default, the tooltip has no style. // Consider importing the stylesheet it comes with: -// 'rc-tooltip/assets/bootstrap_white.css' +// '@rc-component/tooltip/assets/bootstrap_white.css' ReactDOM.render( tooltip}> @@ -135,4 +135,4 @@ npm run coverage ## License -`rc-tooltip` is released under the MIT license. +`@rc-component/tooltip` is released under the MIT license. diff --git a/src/Tooltip.tsx b/src/Tooltip.tsx index e5ac3d0..716f4ea 100644 --- a/src/Tooltip.tsx +++ b/src/Tooltip.tsx @@ -60,7 +60,7 @@ export interface TooltipRef extends TriggerRef {} const Tooltip = React.forwardRef((props, ref) => { const { - trigger = ['hover', 'focus'], + trigger = ['hover'], mouseEnterDelay = 0, mouseLeaveDelay = 0.1, prefixCls = 'rc-tooltip', From 3c7e909208aaa5ab8107f969a6019b176a92fb54 Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Tue, 25 Nov 2025 12:13:21 +0800 Subject: [PATCH 06/12] fix badge urls --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 43f84cf..6cf143b 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ React Tooltip [npm-image]: http://img.shields.io/npm/v/@rc-component/tooltip.svg?style=flat-square [npm-url]: http://npmjs.org/package/@rc-component/tooltip -[travis-image]: https://img.shields.io/travis/@rc-component/tooltip/master?style=flat-square -[travis-url]: https://travis-ci.com/@rc-component/tooltip -[github-actions-image]: https://github.com/@rc-component/tooltip/actions/workflows/react-component-ci.yml/badge.svg -[github-actions-url]: https://github.com/@rc-component/tooltip/actions/workflows/react-component-ci.yml -[codecov-image]: https://img.shields.io/codecov/c/github/@rc-component/tooltip/master.svg?style=flat-square -[codecov-url]: https://app.codecov.io/gh/@rc-component/tooltip +[travis-image]: https://img.shields.io/travis/react-component/tooltip/master?style=flat-square +[travis-url]: https://travis-ci.com/react-component/tooltip +[github-actions-image]: https://github.com/react-component/tooltip/actions/workflows/react-component-ci.yml/badge.svg +[github-actions-url]: https://github.com/react-component/tooltip/actions/workflows/react-component-ci.yml +[codecov-image]: https://img.shields.io/codecov/c/github/react-component/tooltip/master.svg?style=flat-square +[codecov-url]: https://app.codecov.io/gh/react-component/tooltip [download-image]: https://img.shields.io/npm/dm/@rc-component/tooltip.svg?style=flat-square [download-url]: https://npmjs.org/package/@rc-component/tooltip [bundlephobia-url]: https://bundlephobia.com/package/@rc-component/tooltip From 095b9cbc4e8f3fc7e4cf313270bc688c0927a874 Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Thu, 27 Nov 2025 11:20:05 +0800 Subject: [PATCH 07/12] only set aria-describedby when tooltip visible --- src/Tooltip.tsx | 53 +++++++++----------------------------------- tests/index.test.tsx | 19 ++++++++++------ 2 files changed, 22 insertions(+), 50 deletions(-) diff --git a/src/Tooltip.tsx b/src/Tooltip.tsx index 716f4ea..32ebb89 100644 --- a/src/Tooltip.tsx +++ b/src/Tooltip.tsx @@ -4,7 +4,7 @@ import type { ActionType, AlignType } from '@rc-component/trigger/lib/interface' import useId from '@rc-component/util/lib/hooks/useId'; import { clsx } from 'clsx'; import * as React from 'react'; -import { useImperativeHandle, useRef, useEffect, useCallback } from 'react'; +import { useImperativeHandle, useRef, useState } from 'react'; import { placements } from './placements'; import Popup from './Popup'; @@ -90,55 +90,22 @@ const Tooltip = React.forwardRef((props, ref) => { const extraProps: Partial = { ...restProps }; - if ('visible' in props) { + const isControlled = 'visible' in props; + + if (isControlled) { extraProps.popupVisible = props.visible; } - const isControlled = 'visible' in props; - const mergedVisible = props.visible; - const [popupMounted, setPopupMounted] = React.useState(() => { - if (forceRender) { - return true; - } - if (isControlled) { - return mergedVisible; - } - return defaultVisible; - }); - - const updatePopupMounted = useCallback( - (nextVisible: boolean) => { - setPopupMounted((prev) => { - if (nextVisible) { - return true; - } - - if (destroyOnHidden) { - return false; - } - - return prev; - }); - }, - [destroyOnHidden], - ); + const [innerVisible, setInnerVisible] = useState(() => defaultVisible || false); + const mergedVisible = isControlled ? props.visible : innerVisible; const handleVisibleChange = (nextVisible: boolean) => { - updatePopupMounted(nextVisible); + if (!isControlled) { + setInnerVisible(nextVisible); + } onVisibleChange?.(nextVisible); }; - useEffect(() => { - if (forceRender) { - setPopupMounted(true); - return; - } - - if (isControlled) { - updatePopupMounted(mergedVisible); - } - }, [forceRender, isControlled, mergedVisible, updatePopupMounted]); - // ========================= Arrow ========================== // Process arrow configuration const mergedArrow = React.useMemo(() => { @@ -164,7 +131,7 @@ const Tooltip = React.forwardRef((props, ref) => { const originalProps = child?.props || {}; const childProps = { ...originalProps, - 'aria-describedby': overlay && popupMounted ? mergedId : null, + 'aria-describedby': overlay && mergedVisible ? mergedId : null, }; return React.cloneElement(children, childProps) as any; }; diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 342d5ad..57f317a 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -502,7 +502,7 @@ describe('rc-tooltip', () => { }); describe('children handling', () => { - it('should only set aria-describedby once popup is mounted', async () => { + it('should toggle aria-describedby with visibility', async () => { const { container } = render( @@ -519,7 +519,7 @@ describe('rc-tooltip', () => { fireEvent.click(btn); await waitFakeTimers(); - expect(btn).toHaveAttribute('aria-describedby', describedby); + expect(btn).not.toHaveAttribute('aria-describedby'); }); it('should not pass aria-describedby when overlay is empty', () => { @@ -542,17 +542,22 @@ describe('rc-tooltip', () => { expect(container.querySelector('button')).toHaveAttribute('aria-describedby'); }); - it('should set aria-describedby immediately when forceRender is true', () => { + it('should only set aria-describedby for forceRender after visible', async () => { const { container } = render( - + , ); - expect(container.querySelector('button')).toHaveAttribute('aria-describedby'); + const btn = container.querySelector('button'); + expect(btn).not.toHaveAttribute('aria-describedby'); + + fireEvent.click(btn); + await waitFakeTimers(); + expect(btn).toHaveAttribute('aria-describedby'); }); - it('should keep aria-describedby when controlled hidden without destroy', () => { + it('should remove aria-describedby when controlled hidden without destroy', () => { const overlay = 'tooltip content'; const { container, rerender } = render( @@ -568,7 +573,7 @@ describe('rc-tooltip', () => { , ); - expect(container.querySelector('button')).toHaveAttribute('aria-describedby'); + expect(container.querySelector('button')).not.toHaveAttribute('aria-describedby'); }); it('should remove aria-describedby when popup is destroyed on hide', async () => { From e505c88aeffe24f3f9db219828d6c2ac57678bb7 Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Thu, 27 Nov 2025 11:50:20 +0800 Subject: [PATCH 08/12] remove unused urls --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 6cf143b..c5533df 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,6 @@ React Tooltip [npm-image]: http://img.shields.io/npm/v/@rc-component/tooltip.svg?style=flat-square [npm-url]: http://npmjs.org/package/@rc-component/tooltip -[travis-image]: https://img.shields.io/travis/react-component/tooltip/master?style=flat-square -[travis-url]: https://travis-ci.com/react-component/tooltip [github-actions-image]: https://github.com/react-component/tooltip/actions/workflows/react-component-ci.yml/badge.svg [github-actions-url]: https://github.com/react-component/tooltip/actions/workflows/react-component-ci.yml [codecov-image]: https://img.shields.io/codecov/c/github/react-component/tooltip/master.svg?style=flat-square From fe2e01774b4dee1260ce16d48d5587f56674bd55 Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Thu, 27 Nov 2025 16:41:59 +0800 Subject: [PATCH 09/12] use renderProps --- src/Tooltip.tsx | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/Tooltip.tsx b/src/Tooltip.tsx index 32ebb89..2b254ee 100644 --- a/src/Tooltip.tsx +++ b/src/Tooltip.tsx @@ -4,7 +4,7 @@ import type { ActionType, AlignType } from '@rc-component/trigger/lib/interface' import useId from '@rc-component/util/lib/hooks/useId'; import { clsx } from 'clsx'; import * as React from 'react'; -import { useImperativeHandle, useRef, useState } from 'react'; +import { useImperativeHandle, useRef } from 'react'; import { placements } from './placements'; import Popup from './Popup'; @@ -16,13 +16,13 @@ export interface TooltipProps | 'onPopupAlign' | 'builtinPlacements' | 'fresh' - | 'children' | 'mouseLeaveDelay' | 'mouseEnterDelay' | 'prefixCls' | 'forceRender' | 'popupVisible' > { + children: React.ReactElement; // Style classNames?: Partial>; styles?: Partial>; @@ -90,19 +90,11 @@ const Tooltip = React.forwardRef((props, ref) => { const extraProps: Partial = { ...restProps }; - const isControlled = 'visible' in props; - - if (isControlled) { + if ('visible' in props) { extraProps.popupVisible = props.visible; } - const [innerVisible, setInnerVisible] = useState(() => defaultVisible || false); - const mergedVisible = isControlled ? props.visible : innerVisible; - const handleVisibleChange = (nextVisible: boolean) => { - if (!isControlled) { - setInnerVisible(nextVisible); - } onVisibleChange?.(nextVisible); }; @@ -126,14 +118,12 @@ const Tooltip = React.forwardRef((props, ref) => { }, [showArrow, classNames?.arrow, styles?.arrow, arrowContent]); // ======================== Children ======================== - const getChildren = () => { + const getChildren = (open: boolean) => { const child = React.Children.only(children); - const originalProps = child?.props || {}; - const childProps = { - ...originalProps, - 'aria-describedby': overlay && mergedVisible ? mergedId : null, + const ariaProps: React.AriaAttributes = { + 'aria-describedby': overlay && open ? mergedId : undefined, }; - return React.cloneElement(children, childProps) as any; + return React.cloneElement(child, ariaProps); }; // ========================= Render ========================= @@ -172,7 +162,7 @@ const Tooltip = React.forwardRef((props, ref) => { uniqueContainerStyle={styles?.uniqueContainer} {...extraProps} > - {getChildren()} + {({ open }) => getChildren(open)} ); }); From 403dd52fd1eaa23b031c8aea3f020776377de517 Mon Sep 17 00:00:00 2001 From: aojunhao123 <1844749591@qq.com> Date: Thu, 27 Nov 2025 17:18:51 +0800 Subject: [PATCH 10/12] clean code --- src/Tooltip.tsx | 6 +--- tests/index.test.tsx | 66 +------------------------------------------- 2 files changed, 2 insertions(+), 70 deletions(-) diff --git a/src/Tooltip.tsx b/src/Tooltip.tsx index 2b254ee..f3663ce 100644 --- a/src/Tooltip.tsx +++ b/src/Tooltip.tsx @@ -94,10 +94,6 @@ const Tooltip = React.forwardRef((props, ref) => { extraProps.popupVisible = props.visible; } - const handleVisibleChange = (nextVisible: boolean) => { - onVisibleChange?.(nextVisible); - }; - // ========================= Arrow ========================== // Process arrow configuration const mergedArrow = React.useMemo(() => { @@ -148,7 +144,7 @@ const Tooltip = React.forwardRef((props, ref) => { ref={triggerRef} popupAlign={align} getPopupContainer={getTooltipContainer} - onOpenChange={handleVisibleChange} + onOpenChange={onVisibleChange} afterOpenChange={afterVisibleChange} popupMotion={motion} defaultPopupVisible={defaultVisible} diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 57f317a..1363e5e 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -502,36 +502,6 @@ describe('rc-tooltip', () => { }); describe('children handling', () => { - it('should toggle aria-describedby with visibility', async () => { - const { container } = render( - - - , - ); - - const btn = container.querySelector('button'); - expect(btn).not.toHaveAttribute('aria-describedby'); - - fireEvent.click(btn); - await waitFakeTimers(); - const describedby = btn.getAttribute('aria-describedby'); - expect(describedby).toBeTruthy(); - - fireEvent.click(btn); - await waitFakeTimers(); - expect(btn).not.toHaveAttribute('aria-describedby'); - }); - - it('should not pass aria-describedby when overlay is empty', () => { - const { container } = render( - - - , - ); - - expect(container.querySelector('button')).not.toHaveAttribute('aria-describedby'); - }); - it('should set aria-describedby immediately when defaultVisible is true', () => { const { container } = render( @@ -542,22 +512,7 @@ describe('rc-tooltip', () => { expect(container.querySelector('button')).toHaveAttribute('aria-describedby'); }); - it('should only set aria-describedby for forceRender after visible', async () => { - const { container } = render( - - - , - ); - - const btn = container.querySelector('button'); - expect(btn).not.toHaveAttribute('aria-describedby'); - - fireEvent.click(btn); - await waitFakeTimers(); - expect(btn).toHaveAttribute('aria-describedby'); - }); - - it('should remove aria-describedby when controlled hidden without destroy', () => { + it('should remove aria-describedby when controlled hidden', () => { const overlay = 'tooltip content'; const { container, rerender } = render( @@ -576,25 +531,6 @@ describe('rc-tooltip', () => { expect(container.querySelector('button')).not.toHaveAttribute('aria-describedby'); }); - it('should remove aria-describedby when popup is destroyed on hide', async () => { - const { container } = render( - - - , - ); - - const btn = container.querySelector('button'); - expect(btn).not.toHaveAttribute('aria-describedby'); - - fireEvent.click(btn); - await waitFakeTimers(); - expect(btn).toHaveAttribute('aria-describedby'); - - fireEvent.click(btn); - await waitFakeTimers(); - expect(btn).not.toHaveAttribute('aria-describedby'); - }); - it('should preserve original props of children', () => { const onMouseEnter = jest.fn(); From e1082bf83e7642d93080e5c7391cff53ed822cda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 27 Nov 2025 17:55:36 +0800 Subject: [PATCH 11/12] chore: adjust --- package.json | 2 +- src/Tooltip.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index c4c02e7..7d98d1c 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "test": "rc-test" }, "dependencies": { - "@rc-component/trigger": "^3.6.15", + "@rc-component/trigger": "^3.7.1", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, diff --git a/src/Tooltip.tsx b/src/Tooltip.tsx index f3663ce..1d26007 100644 --- a/src/Tooltip.tsx +++ b/src/Tooltip.tsx @@ -114,7 +114,7 @@ const Tooltip = React.forwardRef((props, ref) => { }, [showArrow, classNames?.arrow, styles?.arrow, arrowContent]); // ======================== Children ======================== - const getChildren = (open: boolean) => { + const getChildren: TriggerProps['children'] = ({ open }) => { const child = React.Children.only(children); const ariaProps: React.AriaAttributes = { 'aria-describedby': overlay && open ? mergedId : undefined, @@ -158,7 +158,7 @@ const Tooltip = React.forwardRef((props, ref) => { uniqueContainerStyle={styles?.uniqueContainer} {...extraProps} > - {({ open }) => getChildren(open)} + {getChildren} ); }); From ac227c58641aa2ce860d3e142ce3f2ec20ea8042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Sat, 29 Nov 2025 01:09:39 +0800 Subject: [PATCH 12/12] chore: clean up --- src/Tooltip.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Tooltip.tsx b/src/Tooltip.tsx index 1d26007..9f90bb4 100644 --- a/src/Tooltip.tsx +++ b/src/Tooltip.tsx @@ -79,7 +79,6 @@ const Tooltip = React.forwardRef((props, ref) => { showArrow = true, classNames, styles, - forceRender, ...restProps } = props; @@ -148,7 +147,6 @@ const Tooltip = React.forwardRef((props, ref) => { afterOpenChange={afterVisibleChange} popupMotion={motion} defaultPopupVisible={defaultVisible} - forceRender={forceRender} autoDestroy={destroyOnHidden} mouseLeaveDelay={mouseLeaveDelay} popupStyle={styles?.root}