diff --git a/docs/examples/formError.tsx b/docs/examples/formError.tsx index 68f12a0..4c31f96 100644 --- a/docs/examples/formError.tsx +++ b/docs/examples/formError.tsx @@ -34,7 +34,7 @@ class Test extends Component { visible={this.state.visible} motion={{ motionName: 'rc-tooltip-zoom' }} trigger={[]} - overlayStyle={{ zIndex: 1000 }} + styles={{ root: { zIndex: 1000 } }} overlay={required!} > diff --git a/docs/examples/placement.tsx b/docs/examples/placement.tsx index 087a065..e23454e 100644 --- a/docs/examples/placement.tsx +++ b/docs/examples/placement.tsx @@ -94,8 +94,8 @@ const Test: React.FC = () => (
Debug Usage
Test diff --git a/docs/examples/point.tsx b/docs/examples/point.tsx index 8850763..c476249 100644 --- a/docs/examples/point.tsx +++ b/docs/examples/point.tsx @@ -30,7 +30,7 @@ const Test: React.FC = () => { } > diff --git a/docs/examples/simple.tsx b/docs/examples/simple.tsx index 60afc2f..b3700c1 100644 --- a/docs/examples/simple.tsx +++ b/docs/examples/simple.tsx @@ -217,7 +217,7 @@ class Test extends Component { offset: [this.state.offsetX, this.state.offsetY], }} motion={{ motionName: this.state.transitionName }} - overlayInnerStyle={state.overlayInnerStyle} + styles={{ body: state.overlayInnerStyle }} >
trigger
diff --git a/package.json b/package.json index 380d3ce..57c19c7 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "test": "rc-test" }, "dependencies": { - "@rc-component/trigger": "^3.6.0", + "@rc-component/trigger": "^3.6.4", "@rc-component/util": "^1.3.0", "classnames": "^2.3.1" }, diff --git a/src/Popup.tsx b/src/Popup.tsx index 8f38a4b..04702ac 100644 --- a/src/Popup.tsx +++ b/src/Popup.tsx @@ -1,37 +1,26 @@ -import classNames from 'classnames'; +import cls from 'classnames'; import * as React from 'react'; +import type { TooltipProps } from './Tooltip'; export interface ContentProps { prefixCls?: string; children: (() => React.ReactNode) | React.ReactNode; id?: string; - overlayInnerStyle?: React.CSSProperties; - className?: string; - style?: React.CSSProperties; - bodyClassName?: string; + classNames?: TooltipProps['classNames']; + styles?: TooltipProps['styles']; } const Popup: React.FC = (props) => { - const { - children, - prefixCls, - id, - overlayInnerStyle: innerStyle, - bodyClassName, - className, - style, - } = props; + const { children, prefixCls, id, classNames, styles } = props; return ( -
- + ); }; diff --git a/src/Tooltip.tsx b/src/Tooltip.tsx index bc14731..319915c 100644 --- a/src/Tooltip.tsx +++ b/src/Tooltip.tsx @@ -2,12 +2,14 @@ import type { ArrowType, TriggerProps, TriggerRef } from '@rc-component/trigger' import Trigger from '@rc-component/trigger'; import type { ActionType, AlignType } from '@rc-component/trigger/lib/interface'; import useId from '@rc-component/util/lib/hooks/useId'; -import classNames from 'classnames'; +import cls from 'classnames'; import * as React from 'react'; import { useImperativeHandle, useRef } from 'react'; import { placements } from './placements'; import Popup from './Popup'; +export type SemanticName = 'root' | 'arrow' | 'body'; + export interface TooltipProps extends Pick< TriggerProps, @@ -21,30 +23,32 @@ export interface TooltipProps | 'forceRender' | 'popupVisible' > { + // Style + classNames?: Partial>; + styles?: Partial>; + + /** Config popup motion */ + motion?: TriggerProps['popupMotion']; + + // Rest trigger?: ActionType | ActionType[]; defaultVisible?: boolean; visible?: boolean; placement?: string; - /** Config popup motion */ - motion?: TriggerProps['popupMotion']; + onVisibleChange?: (visible: boolean) => void; afterVisibleChange?: (visible: boolean) => void; overlay: (() => React.ReactNode) | React.ReactNode; - /** @deprecated Please use `styles={{ root: {} }}` */ - overlayStyle?: React.CSSProperties; - /** @deprecated Please use `classNames={{ root: '' }}` */ - overlayClassName?: string; + getTooltipContainer?: (node: HTMLElement) => HTMLElement; destroyOnHidden?: boolean; align?: AlignType; showArrow?: boolean | ArrowType; arrowContent?: React.ReactNode; id?: string; - /** @deprecated Please use `styles={{ body: {} }}` */ - overlayInnerStyle?: React.CSSProperties; + zIndex?: number; - styles?: TooltipStyles; - classNames?: TooltipClassNames; + /** * Configures Tooltip to reuse the background for transition usage. * This is an experimental API and may not be stable. @@ -52,25 +56,13 @@ export interface TooltipProps unique?: TriggerProps['unique']; } -export interface TooltipStyles { - root?: React.CSSProperties; - body?: React.CSSProperties; -} - -export interface TooltipClassNames { - root?: string; - body?: string; -} - export interface TooltipRef extends TriggerRef {} const Tooltip = React.forwardRef((props, ref) => { const { - overlayClassName, trigger = ['hover'], mouseEnterDelay = 0, mouseLeaveDelay = 0.1, - overlayStyle, prefixCls = 'rc-tooltip', children, onVisibleChange, @@ -81,13 +73,12 @@ const Tooltip = React.forwardRef((props, ref) => { destroyOnHidden = false, defaultVisible, getTooltipContainer, - overlayInnerStyle, arrowContent, overlay, id, showArrow = true, - classNames: tooltipClassNames, - styles: tooltipStyles, + classNames, + styles, ...restProps } = props; @@ -102,18 +93,26 @@ const Tooltip = React.forwardRef((props, ref) => { extraProps.popupVisible = props.visible; } - const getPopupElement = () => ( - - {overlay} - - ); + // ========================= Arrow ========================== + // Process arrow configuration + const mergedArrow = React.useMemo(() => { + if (!showArrow) { + return false; + } + + // Convert true to object for unified processing + const arrowConfig = showArrow === true ? {} : showArrow; + + // Apply semantic styles with unified logic + return { + ...arrowConfig, + className: cls(arrowConfig.className, classNames?.arrow), + style: { ...arrowConfig.style, ...styles?.arrow }, + content: arrowConfig.content ?? arrowContent, + }; + }, [showArrow, classNames?.arrow, styles?.arrow, arrowContent]); + // ======================== Children ======================== const getChildren = () => { const child = React.Children.only(children); const originalProps = child?.props || {}; @@ -124,11 +123,22 @@ const Tooltip = React.forwardRef((props, ref) => { return React.cloneElement(children, childProps) as any; }; + // ========================= Render ========================= return ( + {overlay} + + } action={trigger} builtinPlacements={placements} popupPlacement={placement} @@ -141,9 +151,9 @@ const Tooltip = React.forwardRef((props, ref) => { defaultPopupVisible={defaultVisible} autoDestroy={destroyOnHidden} mouseLeaveDelay={mouseLeaveDelay} - popupStyle={{ ...overlayStyle, ...tooltipStyles?.root }} + popupStyle={styles?.root} mouseEnterDelay={mouseEnterDelay} - arrow={showArrow} + arrow={mergedArrow} {...extraProps} > {getChildren()} diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 9586008..fd6511a 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -1,6 +1,6 @@ import { act, fireEvent, render } from '@testing-library/react'; import React from 'react'; -import Tooltip, { TooltipRef } from '../src'; +import Tooltip, { type TooltipRef } from '../src'; const verifyContent = (wrapper: HTMLElement, content: string) => { expect(wrapper.querySelector('.x-content').textContent).toBe(content); @@ -59,23 +59,7 @@ describe('rc-tooltip', () => { verifyContent(container, 'Tooltip content'); }); - // https://github.com/ant-design/ant-design/pull/23155 - it('using style inner style', () => { - const { container } = render( - Tooltip content} - overlayInnerStyle={{ background: 'red' }} - > -
Click this
-
, - ); - fireEvent.click(container.querySelector('.target')); - expect( - (container.querySelector('.rc-tooltip-inner') as HTMLElement).style.background, - ).toEqual('red'); - }); + it('access of ref', () => { const domRef = React.createRef(); @@ -216,6 +200,87 @@ describe('rc-tooltip', () => { ); expect(container.querySelector('.rc-tooltip-arrow')).toBeFalsy(); }); + + it('should merge arrow className from showArrow and classNames.arrow', () => { + const { container } = render( + Tooltip content} + showArrow={{ + className: 'from-showArrow', + }} + classNames={{ + arrow: 'from-classNames', + }} + visible + > +
Click this
+
, + ); + + const arrowElement = container.querySelector('.rc-tooltip-arrow'); + expect(arrowElement).toHaveClass('from-showArrow'); + expect(arrowElement).toHaveClass('from-classNames'); + }); + + it('should use arrowContent from showArrow object', () => { + const { container } = render( + Tooltip content} + showArrow={{ + content: , + }} + visible + > +
Click this
+
, + ); + + expect(container.querySelector('.custom-arrow-content')).toBeTruthy(); + expect(container.querySelector('.custom-arrow-content').textContent).toBe('↑'); + }); + + it('should use arrowContent prop when showArrow has no content', () => { + const { container } = render( + Tooltip content} + showArrow + arrowContent={} + visible + > +
Click this
+
, + ); + + expect(container.querySelector('.prop-arrow-content')).toBeTruthy(); + expect(container.querySelector('.prop-arrow-content').textContent).toBe('→'); + }); + + it('should prioritize showArrow.content over arrowContent prop', () => { + const { container } = render( + Tooltip content} + showArrow={{ + content: , + }} + arrowContent={} + visible + > +
Click this
+
, + ); + + expect(container.querySelector('.showArrow-content')).toBeTruthy(); + expect(container.querySelector('.prop-content')).toBeFalsy(); + expect(container.querySelector('.showArrow-content').textContent).toBe('↑'); + }); }); it('visible', () => { @@ -251,33 +316,173 @@ describe('rc-tooltip', () => { expect(nodeRef.current.nativeElement).toBe(container.querySelector('button')); }); - it('should apply custom styles to Tooltip', () => { - const customClassNames = { - body: 'custom-body', - root: 'custom-root', - }; + describe('classNames and styles', () => { + it('should apply custom classNames to all semantic elements', () => { + const customClassNames = { + root: 'custom-root', + body: 'custom-body', + arrow: 'custom-arrow', + }; - const customStyles = { - body: { color: 'red' }, - root: { backgroundColor: 'blue' }, - }; + const { container } = render( + Tooltip content
} + visible + showArrow + > + + , + ); - const { container } = render( - } styles={customStyles} visible> - + , + ); + + const tooltipElement = container.querySelector('.rc-tooltip') as HTMLElement; + const tooltipBodyElement = container.querySelector('.rc-tooltip-body') as HTMLElement; + const tooltipArrowElement = container.querySelector('.rc-tooltip-arrow') as HTMLElement; + + // Verify styles + expect(tooltipElement).toHaveStyle({ backgroundColor: 'blue', zIndex: '1000' }); + expect(tooltipBodyElement).toHaveStyle({ color: 'red', fontSize: '14px' }); + expect(tooltipArrowElement).toHaveStyle({ borderColor: 'green' }); + }); - const tooltipElement = container.querySelector('.rc-tooltip') as HTMLElement; - const tooltipBodyElement = container.querySelector('.rc-tooltip-inner') as HTMLElement; + it('should apply both classNames and styles simultaneously', () => { + const customClassNames = { + root: 'custom-root', + body: 'custom-body', + arrow: 'custom-arrow', + }; - // 验证 classNames - expect(tooltipElement.classList).toContain('custom-root'); - expect(tooltipBodyElement.classList).toContain('custom-body'); + const customStyles = { + root: { backgroundColor: 'blue' }, + body: { color: 'red' }, + arrow: { borderColor: 'green' }, + }; - // 验证 styles - expect(tooltipElement.style.backgroundColor).toBe('blue'); - expect(tooltipBodyElement.style.color).toBe('red'); + const { container } = render( + Tooltip content} + visible + showArrow + > + + , + ); + + const tooltipElement = container.querySelector('.rc-tooltip') as HTMLElement; + const tooltipBodyElement = container.querySelector('.rc-tooltip-body') as HTMLElement; + const tooltipArrowElement = container.querySelector('.rc-tooltip-arrow') as HTMLElement; + + // Verify that classNames and styles work simultaneously + expect(tooltipElement).toHaveClass('custom-root'); + expect(tooltipElement).toHaveStyle({ backgroundColor: 'blue' }); + expect(tooltipBodyElement).toHaveClass('custom-body'); + expect(tooltipBodyElement).toHaveStyle({ color: 'red' }); + expect(tooltipArrowElement).toHaveClass('custom-arrow'); + expect(tooltipArrowElement).toHaveStyle({ borderColor: 'green' }); + }); + + it('should work with partial classNames and styles', () => { + const partialClassNames = { + body: 'custom-body', + }; + + const partialStyles = { + root: { backgroundColor: 'blue' }, + }; + + const { container } = render( + Tooltip content} + visible + showArrow + > + + , + ); + + const tooltipElement = container.querySelector('.rc-tooltip') as HTMLElement; + const tooltipBodyElement = container.querySelector('.rc-tooltip-body') as HTMLElement; + const tooltipArrowElement = container.querySelector('.rc-tooltip-arrow') as HTMLElement; + + // Verify partial configuration takes effect + expect(tooltipElement).toHaveStyle({ backgroundColor: 'blue' }); + expect(tooltipBodyElement).toHaveClass('custom-body'); + + // Verify that unconfigured elements don't have custom class names or styles + expect(tooltipElement).not.toHaveClass('custom-root'); + expect(tooltipArrowElement).not.toHaveClass('custom-arrow'); + }); + + it('should not break when showArrow is false', () => { + const customClassNames = { + root: 'custom-root', + body: 'custom-body', + arrow: 'custom-arrow', // 即使配置了arrow,但不显示箭头时不应该报错 + }; + + const customStyles = { + root: { backgroundColor: 'blue' }, + body: { color: 'red' }, + arrow: { borderColor: 'green' }, + }; + + const { container } = render( + Tooltip content} + visible + showArrow={false} + > + + , + ); + + const tooltipElement = container.querySelector('.rc-tooltip') as HTMLElement; + const tooltipBodyElement = container.querySelector('.rc-tooltip-body') as HTMLElement; + const tooltipArrowElement = container.querySelector('.rc-tooltip-arrow'); + + // Verify when arrow is not shown + expect(tooltipArrowElement).toBeFalsy(); + + // Other styles still take effect + expect(tooltipElement).toHaveClass('custom-root'); + expect(tooltipElement).toHaveStyle({ backgroundColor: 'blue' }); + expect(tooltipBodyElement).toHaveClass('custom-body'); + expect(tooltipBodyElement).toHaveStyle({ color: 'red' }); + }); }); describe('children handling', () => { @@ -315,7 +520,7 @@ describe('rc-tooltip', () => { const btn = container.querySelector('button'); expect(btn).toHaveClass('custom-btn'); - // 触发原始事件处理器 + // Trigger original event handler fireEvent.mouseEnter(btn); expect(onMouseEnter).toHaveBeenCalled(); });