Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# rc-tooltip
# @rc-component/tooltip

React Tooltip

Expand All @@ -9,18 +9,16 @@ 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
[npm-image]: http://img.shields.io/npm/v/@rc-component/tooltip.svg?style=flat-square
[npm-url]: http://npmjs.org/package/@rc-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
[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

Expand All @@ -36,18 +34,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 placement="left" trigger={['click']} overlay={<span>tooltip</span>}>
Expand Down Expand Up @@ -135,4 +133,4 @@ npm run coverage

## License

`rc-tooltip` is released under the MIT license.
`@rc-component/tooltip` is released under the MIT license.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,14 @@
"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"
},
"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",
Expand Down
14 changes: 6 additions & 8 deletions src/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ export interface TooltipProps
| 'onPopupAlign'
| 'builtinPlacements'
| 'fresh'
| 'children'
| 'mouseLeaveDelay'
| 'mouseEnterDelay'
| 'prefixCls'
| 'forceRender'
| 'popupVisible'
> {
children: React.ReactElement;
// Style
classNames?: Partial<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
Expand Down Expand Up @@ -113,14 +113,12 @@ const Tooltip = React.forwardRef<TooltipRef, TooltipProps>((props, ref) => {
}, [showArrow, classNames?.arrow, styles?.arrow, arrowContent]);

// ======================== Children ========================
const getChildren = () => {
const getChildren: TriggerProps['children'] = ({ open }) => {
const child = React.Children.only(children);
const originalProps = child?.props || {};
const childProps = {
...originalProps,
'aria-describedby': overlay ? mergedId : null,
const ariaProps: React.AriaAttributes = {
'aria-describedby': overlay && open ? mergedId : undefined,
};
return React.cloneElement<any>(children, childProps) as any;
return React.cloneElement(child, ariaProps);
};
Comment on lines +116 to 122
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

修复 aria-describedby 属性覆盖问题。

React.cloneElement(child, ariaProps) 会直接覆盖子元素已有的 aria-describedby 属性,破坏子元素自身的无障碍关联关系。根据 WAI-ARIA 规范,应使用空格分隔的 ID 列表合并多个描述源。

应用此 diff 以正确合并 aria-describedby:

  const getChildren: TriggerProps['children'] = ({ open }) => {
    const child = React.Children.only(children);
+   const existingDescribedBy = child.props['aria-describedby'];
+   const tooltipDescribedBy = overlay && open ? mergedId : undefined;
+   const mergedDescribedBy = 
+     tooltipDescribedBy && existingDescribedBy
+       ? `${existingDescribedBy} ${tooltipDescribedBy}`
+       : tooltipDescribedBy || existingDescribedBy;
    const ariaProps: React.AriaAttributes = {
-     'aria-describedby': overlay && open ? mergedId : undefined,
+     'aria-describedby': mergedDescribedBy,
    };
    return React.cloneElement(child, ariaProps);
  };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getChildren: TriggerProps['children'] = ({ open }) => {
const child = React.Children.only(children);
const originalProps = child?.props || {};
const childProps = {
...originalProps,
'aria-describedby': overlay ? mergedId : null,
const ariaProps: React.AriaAttributes = {
'aria-describedby': overlay && open ? mergedId : undefined,
};
return React.cloneElement<any>(children, childProps) as any;
return React.cloneElement(child, ariaProps);
};
const getChildren: TriggerProps['children'] = ({ open }) => {
const child = React.Children.only(children);
const existingDescribedBy = child.props['aria-describedby'];
const tooltipDescribedBy = overlay && open ? mergedId : undefined;
const mergedDescribedBy =
tooltipDescribedBy && existingDescribedBy
? `${existingDescribedBy} ${tooltipDescribedBy}`
: tooltipDescribedBy || existingDescribedBy;
const ariaProps: React.AriaAttributes = {
'aria-describedby': mergedDescribedBy,
};
return React.cloneElement(child, ariaProps);
};
🤖 Prompt for AI Agents
In src/Tooltip.tsx around lines 116 to 122, React.cloneElement(child, ariaProps)
currently overwrites any existing child aria-describedby; instead read the
child's existing aria-describedby (child.props &&
child.props['aria-describedby']), and if overlay && open and mergedId exist,
merge them by space-separating the existing value and mergedId (deduplicate if
present), then pass the combined string (or undefined if empty) into
cloneElement so existing aria-describedby values are preserved and multiple IDs
follow WAI-ARIA space-separated list rules.


// ========================= Render =========================
Expand Down Expand Up @@ -158,7 +156,7 @@ const Tooltip = React.forwardRef<TooltipRef, TooltipProps>((props, ref) => {
uniqueContainerStyle={styles?.uniqueContainer}
{...extraProps}
>
{getChildren()}
{getChildren}
</Trigger>
);
});
Expand Down
21 changes: 15 additions & 6 deletions tests/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -502,19 +502,28 @@ describe('rc-tooltip', () => {
});

describe('children handling', () => {
it('should pass aria-describedby to child element when overlay exists', () => {
it('should set aria-describedby immediately when defaultVisible is true', () => {
const { container } = render(
<Tooltip id="test-id" overlay="tooltip content">
<Tooltip defaultVisible overlay="tooltip content">
<button>Click me</button>
</Tooltip>,
);

expect(container.querySelector('button')).toHaveAttribute('aria-describedby', 'test-id');
expect(container.querySelector('button')).toHaveAttribute('aria-describedby');
});

it('should not pass aria-describedby when overlay is empty', () => {
const { container } = render(
<Tooltip id="test-id" overlay={null}>
it('should remove aria-describedby when controlled hidden', () => {
const overlay = 'tooltip content';
const { container, rerender } = render(
<Tooltip overlay={overlay} visible>
<button>Click me</button>
</Tooltip>,
);

expect(container.querySelector('button')).toHaveAttribute('aria-describedby');

rerender(
<Tooltip overlay={overlay} visible={false}>
<button>Click me</button>
</Tooltip>,
);
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"declaration": true,
"skipLibCheck": true,
"esModuleInterop": true,
"types": ["@testing-library/jest-dom"],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

By adding the types property, you override TypeScript's default type inclusion. This means that other ambient type definitions, like for Jest (jest) and Node.js (node), will no longer be automatically included, which will likely break your build. You need to explicitly list all required ambient types.1

Suggested change
"types": ["@testing-library/jest-dom"],
"types": ["jest", "node", "@testing-library/jest-dom"],

Rules References

Footnotes

  1. When using the compilerOptions.types property in tsconfig.json, TypeScript will only include the type definitions for the packages explicitly listed in the array. This overrides the default behavior of automatically including all packages found in node_modules/@types. Therefore, you must list all necessary type definitions, such as jest and node, to avoid compilation errors.

"paths": {
"@/*": [
"src/*"
Expand Down