Skip to content
Merged
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
5 changes: 5 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
testEnvironment: 'jsdom',
setupFiles: ['<rootDir>/tests/setup.js'],
};

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,14 @@
"dependencies": {
"@babel/runtime": "^7.11.2",
"@rc-component/trigger": "^2.0.0",
"classnames": "^2.3.1"
"classnames": "^2.3.1",
"rc-util": "^5.44.3"
},
"devDependencies": {
"@rc-component/father-plugin": "^1.0.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.4.0",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
Expand All @@ -69,4 +72,4 @@
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
}
}
49 changes: 40 additions & 9 deletions src/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,51 @@ export interface ContentProps {
bodyClassName?: string;
}

const getTextContent = (node: (() => React.ReactNode) | React.ReactNode): string => {
if (!node) {
return '';
}

const resolvedNode = typeof node === 'function' ? node() : node;

if (typeof resolvedNode === 'string' || typeof resolvedNode === 'number') {
return String(resolvedNode);
}

if (Array.isArray(resolvedNode)) {
return resolvedNode.map(getTextContent).join(' ');
}

if (React.isValidElement(resolvedNode)) {
return getTextContent(resolvedNode.props.children);
}
};

export default function Popup(props: ContentProps) {
const { children, prefixCls, id, overlayInnerStyle: innerStyle, bodyClassName, className, style } =
props;

const tooltipText = getTextContent(children);

return (
<div className={classNames(`${prefixCls}-content`, className)} style={style}>
<div
className={classNames(`${prefixCls}-inner`, bodyClassName)}
id={id}
role="tooltip"
style={innerStyle}
>
{typeof children === 'function' ? children() : children}
<>
<div className={classNames(`${prefixCls}-content`, className)} style={style}>
<div
className={classNames(`${prefixCls}-inner`, bodyClassName)}
style={innerStyle}
>
{typeof children === 'function' ? children() : children}
</div>
</div>
</div>
{tooltipText && (
<div
id={id}
role="tooltip"
style={{ width: 0, height: 0, position: 'absolute', overflow: 'hidden', opacity: 0 }}
>
{tooltipText}
</div>
)}
</>
);
}
24 changes: 19 additions & 5 deletions src/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { ArrowType, TriggerProps, TriggerRef } from '@rc-component/trigger';
import Trigger from '@rc-component/trigger';
import type { ActionType, AlignType, AnimationType } from '@rc-component/trigger/lib/interface';
import classNames from 'classnames';
import * as React from 'react';
import { forwardRef, useImperativeHandle, useRef } from 'react';
import { forwardRef, useImperativeHandle, useRef, cloneElement } from 'react';
import { placements } from './placements';
import Popup from './Popup';
import classNames from 'classnames';
import useId from 'rc-util/lib/hooks/useId';

export interface TooltipProps
extends Pick<
Expand Down Expand Up @@ -60,9 +61,11 @@ export interface TooltipClassNames {
body?: string;
}

export interface TooltipRef extends TriggerRef {}
export interface TooltipRef extends TriggerRef { }

const Tooltip = (props: TooltipProps, ref: React.Ref<TooltipRef>) => {
const defaultId = useId();

const {
overlayClassName,
trigger = ['hover'],
Expand All @@ -84,7 +87,7 @@ const Tooltip = (props: TooltipProps, ref: React.Ref<TooltipRef>) => {
overlayInnerStyle,
arrowContent,
overlay,
id,
id = defaultId,
showArrow = true,
classNames: tooltipClassNames,
styles: tooltipStyles,
Expand All @@ -111,6 +114,17 @@ const Tooltip = (props: TooltipProps, ref: React.Ref<TooltipRef>) => {
</Popup>
);

const getChildren = () => {
const originalProps = (children as React.ReactElement)?.props || {};

const childProps = {
...originalProps,
'aria-describedby': overlay ? id : null,
};

return cloneElement(children, childProps);
};

return (
<Trigger
popupClassName={classNames(overlayClassName, tooltipClassNames?.root)}
Expand All @@ -135,7 +149,7 @@ const Tooltip = (props: TooltipProps, ref: React.Ref<TooltipRef>) => {
arrow={showArrow}
{...extraProps}
>
{children}
{getChildren()}
</Trigger>
);
};
Expand Down
94 changes: 93 additions & 1 deletion tests/popup.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,100 @@
import { Popup } from '../src';
import React from 'react';
import { render } from '@testing-library/react';
import Popup from '../src/Popup';

describe('Popup', () => {
// Used in antd for C2D2C
it('should export', () => {
expect(Popup).toBeTruthy();
});

it('should correctly extract text from string, number, function, and element', () => {
const { getByRole } = render(
<Popup prefixCls="test" id="popup-id">
{() => (
<>
{'Hello'}
{123}
<span>World</span>
</>
)}
</Popup>,
);

const tooltip = getByRole('tooltip');
const hiddenTextContainer = tooltip.querySelector('div > div');

expect(hiddenTextContainer.textContent).toBe('Hello 123 World');
});

it('should apply updated hidden text styles correctly', () => {
const { getByRole } = render(
<Popup prefixCls="test" id="popup-id">
test hidden text
</Popup>,
);

const tooltip = getByRole('tooltip');
const hiddenTextContainer = tooltip.querySelector('div > div');

expect(hiddenTextContainer).toHaveStyle({
width: '0',
height: '0',
position: 'absolute',
overflow: 'hidden',
opacity: '0',
});
});

it('should return empty string if children is null or undefined', () => {
const { getByRole } = render(
<Popup prefixCls="test" id="popup-empty">
{null}
</Popup>,
);
const tooltip = getByRole('tooltip');

expect(tooltip.querySelector('div > div')).toBeNull();
});

it('should handle nested arrays correctly', () => {
const { getByRole } = render(
<Popup prefixCls="test" id="popup-nested">
{[
'First',
['Second', 'Third'],
<span key="fourth">Fourth</span>,
]}
</Popup>,
);
const tooltip = getByRole('tooltip');
const hiddenTextContainer = tooltip.querySelector('div > div');

// "First Second Third Fourth"
expect(hiddenTextContainer.textContent).toBe('First Second Third Fourth');
});

it('should handle function returning an array', () => {
const { getByRole } = render(
<Popup prefixCls="test" id="popup-func-array">
{() => ['Alpha', <span key="beta">Beta</span>]}
</Popup>,
);
const tooltip = getByRole('tooltip');
const hiddenTextContainer = tooltip.querySelector('div > div');

// "Alpha Beta"
expect(hiddenTextContainer.textContent).toBe('Alpha Beta');
});

it('should handle function returning undefined', () => {
const { getByRole } = render(
<Popup prefixCls="test" id="popup-func-undefined">
{() => undefined}
</Popup>,
);
const tooltip = getByRole('tooltip');

expect(tooltip.querySelector('div > div')).toBeNull();
});
});
2 changes: 1 addition & 1 deletion tests/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ if (typeof window !== 'undefined') {
global.window.innerHeight = height || global.window.innerHeight;
global.window.dispatchEvent(new Event('resize'));
};
global.window.scrollTo = () => {};
global.window.scrollTo = () => { };
// ref: https://github.com/ant-design/ant-design/issues/18774
if (!window.matchMedia) {
Object.defineProperty(global.window, 'matchMedia', {
Expand Down