Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
53 changes: 50 additions & 3 deletions src/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export interface TooltipRef extends TriggerRef {}

const Tooltip = React.forwardRef<TooltipRef, TooltipProps>((props, ref) => {
const {
trigger = ['hover'],
trigger = ['hover','focus'],
mouseEnterDelay = 0,
mouseLeaveDelay = 0.1,
prefixCls = 'rc-tooltip',
Expand All @@ -79,6 +79,7 @@ const Tooltip = React.forwardRef<TooltipRef, TooltipProps>((props, ref) => {
showArrow = true,
classNames,
styles,
forceRender,
...restProps
} = props;

Expand All @@ -93,6 +94,51 @@ const Tooltip = React.forwardRef<TooltipRef, TooltipProps>((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(() => {
Expand All @@ -118,7 +164,7 @@ const Tooltip = React.forwardRef<TooltipRef, TooltipProps>((props, ref) => {
const originalProps = child?.props || {};
const childProps = {
...originalProps,
'aria-describedby': overlay ? mergedId : null,
'aria-describedby': overlay && popupMounted ? mergedId : null,
};
return React.cloneElement<any>(children, childProps) as any;
};
Expand All @@ -145,10 +191,11 @@ const Tooltip = React.forwardRef<TooltipRef, TooltipProps>((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}
Expand Down
55 changes: 52 additions & 3 deletions tests/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Tooltip id="test-id" overlay="tooltip content">
<Tooltip trigger={['click']} overlay="tooltip content">
<button>Click me</button>
</Tooltip>,
);

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', () => {
Expand All @@ -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(
<Tooltip defaultVisible overlay="tooltip content">
<button>Click me</button>
</Tooltip>,
);

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

it('should set aria-describedby immediately when forceRender is true', () => {
const { container } = render(
<Tooltip forceRender overlay="tooltip content">
<button>Click me</button>
</Tooltip>,
);

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

it('should remove aria-describedby when popup is destroyed on hide', async () => {
const { container } = render(
<Tooltip destroyOnHidden trigger={['click']} overlay="tooltip content">
<button>Click me</button>
</Tooltip>,
);

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();

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