index.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. /* eslint-disable prefer-destructuring, max-lines-per-function, react/no-find-dom-node, max-len, @typescript-eslint/no-empty-function */
  2. import React, { isValidElement, cloneElement } from 'react';
  3. import ReactDOM from 'react-dom';
  4. import classNames from 'classnames';
  5. import PropTypes from 'prop-types';
  6. import { throttle, noop, get, omit, each, isEmpty } from 'lodash-es';
  7. import { BASE_CLASS_PREFIX } from '@douyinfe/semi-foundation/base/constants';
  8. import warning from '@douyinfe/semi-foundation/utils/warning';
  9. import Event from '@douyinfe/semi-foundation/utils/Event';
  10. import { ArrayElement } from '@douyinfe/semi-foundation/utils/type';
  11. import { convertDOMRectToObject, DOMRectLikeType } from '@douyinfe/semi-foundation/utils/dom';
  12. import TooltipFoundation, { TooltipAdapter, Position, PopupContainerDOMRect } from '@douyinfe/semi-foundation/tooltip/foundation';
  13. import { strings, cssClasses, numbers } from '@douyinfe/semi-foundation/tooltip/constants';
  14. import '@douyinfe/semi-foundation/tooltip/tooltip.scss';
  15. import BaseComponent, { BaseProps } from '../_base/baseComponent';
  16. import { isHTMLElement } from '../_base/reactUtils';
  17. import { stopPropagation } from '../_utils';
  18. import Portal from '../_portal/index';
  19. import ConfigContext from '../configProvider/context';
  20. import TriangleArrow from './TriangleArrow';
  21. import TriangleArrowVertical from './TriangleArrowVertical';
  22. import TooltipTransition from './TooltipStyledTransition';
  23. import ArrowBoundingShape from './ArrowBoundingShape';
  24. import { Motion } from '../_base/base';
  25. export { TooltipTransitionProps } from './TooltipStyledTransition';
  26. export type Trigger = ArrayElement<typeof strings.TRIGGER_SET>;
  27. export interface ArrowBounding {
  28. offsetX?: number;
  29. offsetY?: number;
  30. width?: number;
  31. height?: number;
  32. }
  33. export interface TooltipProps extends BaseProps {
  34. children?: React.ReactNode;
  35. motion?: Motion;
  36. autoAdjustOverflow?: boolean;
  37. position?: Position;
  38. getPopupContainer?: () => HTMLElement;
  39. mouseEnterDelay?: number;
  40. mouseLeaveDelay?: number;
  41. trigger?: Trigger;
  42. className?: string;
  43. clickToHide?: boolean;
  44. visible?: boolean;
  45. style?: React.CSSProperties;
  46. content?: React.ReactNode;
  47. prefixCls?: string;
  48. onVisibleChange?: (visible: boolean) => void;
  49. onClickOutSide?: (e: React.MouseEvent) => void;
  50. spacing?: number;
  51. showArrow?: boolean | React.ReactNode;
  52. zIndex?: number;
  53. rePosKey?: string | number;
  54. arrowBounding?: ArrowBounding;
  55. transformFromCenter?: boolean;
  56. arrowPointAtCenter?: boolean;
  57. wrapWhenSpecial?: boolean;
  58. stopPropagation?: boolean;
  59. clickTriggerToHide?: boolean;
  60. wrapperClassName?: string;
  61. }
  62. interface TooltipState {
  63. visible: boolean;
  64. transitionState: string;
  65. triggerEventSet: {
  66. [key: string]: any;
  67. };
  68. portalEventSet: {
  69. [key: string]: any;
  70. };
  71. containerStyle: React.CSSProperties;
  72. isInsert: boolean;
  73. placement: Position;
  74. transitionStyle: Record<string, any>;
  75. }
  76. const prefix = cssClasses.PREFIX;
  77. const positionSet = strings.POSITION_SET;
  78. const triggerSet = strings.TRIGGER_SET;
  79. const blockDisplays = ['flex', 'block', 'table', 'flow-root', 'grid'];
  80. const defaultGetContainer = () => document.body;
  81. export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
  82. static contextType = ConfigContext;
  83. static propTypes = {
  84. children: PropTypes.node,
  85. motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.object, PropTypes.func]),
  86. autoAdjustOverflow: PropTypes.bool,
  87. position: PropTypes.oneOf(positionSet),
  88. getPopupContainer: PropTypes.func,
  89. mouseEnterDelay: PropTypes.number,
  90. mouseLeaveDelay: PropTypes.number,
  91. trigger: PropTypes.oneOf(triggerSet).isRequired,
  92. className: PropTypes.string,
  93. wrapperClassName: PropTypes.string,
  94. clickToHide: PropTypes.bool,
  95. // used with trigger === hover, private
  96. clickTriggerToHide: PropTypes.bool,
  97. visible: PropTypes.bool,
  98. style: PropTypes.object,
  99. content: PropTypes.node,
  100. prefixCls: PropTypes.string,
  101. onVisibleChange: PropTypes.func,
  102. onClickOutSide: PropTypes.func,
  103. spacing: PropTypes.number,
  104. showArrow: PropTypes.oneOfType([PropTypes.bool, PropTypes.node]),
  105. zIndex: PropTypes.number,
  106. rePosKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  107. arrowBounding: ArrowBoundingShape,
  108. transformFromCenter: PropTypes.bool, // Whether to change from the center of the trigger (for dynamic effects)
  109. arrowPointAtCenter: PropTypes.bool,
  110. stopPropagation: PropTypes.bool,
  111. // private
  112. wrapWhenSpecial: PropTypes.bool, // when trigger has special status such as "disabled" or "loading", wrap span
  113. };
  114. static defaultProps = {
  115. transformFromCenter: true,
  116. arrowPointAtCenter: true,
  117. wrapWhenSpecial: true,
  118. motion: true,
  119. zIndex: numbers.DEFAULT_Z_INDEX,
  120. trigger: 'hover',
  121. position: 'top',
  122. prefixCls: prefix,
  123. autoAdjustOverflow: true,
  124. mouseEnterDelay: numbers.MOUSE_ENTER_DELAY,
  125. mouseLeaveDelay: numbers.MOUSE_LEAVE_DELAY,
  126. onVisibleChange: noop,
  127. onClickOutSide: noop,
  128. spacing: numbers.SPACING,
  129. showArrow: true,
  130. arrowBounding: numbers.ARROW_BOUNDING,
  131. };
  132. eventManager: Event;
  133. triggerEl: React.RefObject<unknown>;
  134. containerEl: React.RefObject<unknown>;
  135. clickOutsideHandler: any;
  136. resizeHandler: any;
  137. isWrapped: boolean;
  138. mounted: any;
  139. scrollHandler: any;
  140. getPopupContainer: () => HTMLElement;
  141. containerPosition: string;
  142. constructor(props: TooltipProps) {
  143. super(props);
  144. this.state = {
  145. visible: false,
  146. /**
  147. *
  148. * Note: The transitionState parameter is equivalent to isInsert
  149. */
  150. transitionState: '',
  151. triggerEventSet: {},
  152. portalEventSet: {},
  153. containerStyle: {
  154. // zIndex: props.zIndex,
  155. },
  156. isInsert: false,
  157. placement: props.position || 'top',
  158. transitionStyle: {},
  159. };
  160. this.foundation = new TooltipFoundation(this.adapter);
  161. this.eventManager = new Event();
  162. this.triggerEl = React.createRef();
  163. this.containerEl = React.createRef();
  164. this.clickOutsideHandler = null;
  165. this.resizeHandler = null;
  166. this.isWrapped = false; // Identifies whether a span element is wrapped
  167. this.containerPosition = undefined;
  168. }
  169. setContainerEl = (node: HTMLDivElement) => (this.containerEl = { current: node });
  170. get adapter(): TooltipAdapter<TooltipProps, TooltipState> {
  171. return {
  172. ...super.adapter,
  173. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  174. // @ts-ignore
  175. on: (...args: any[]) => this.eventManager.on(...args),
  176. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  177. // @ts-ignore
  178. off: (...args: any[]) => this.eventManager.off(...args),
  179. insertPortal: (content: string, { position, ...containerStyle }: { position: Position }) => {
  180. this.setState(
  181. {
  182. isInsert: true,
  183. transitionState: 'enter',
  184. containerStyle: { ...this.state.containerStyle, ...containerStyle },
  185. },
  186. () => {
  187. /**
  188. * Dangerous: remove setTimeout from here fix #1301
  189. * setTimeout may emit portalInserted event after hiding portal
  190. * Hiding portal will remove portalInserted event listener(normal process)
  191. * then portal can't hide because _togglePortalVisible(false) will found isVisible=false and nowVisible=false(bug here)
  192. */
  193. this.eventManager.emit('portalInserted');
  194. }
  195. );
  196. },
  197. removePortal: () => {
  198. this.setState({ isInsert: false });
  199. },
  200. getEventName: () => ({
  201. mouseEnter: 'onMouseEnter',
  202. mouseLeave: 'onMouseLeave',
  203. mouseOut: 'onMouseOut',
  204. mouseOver: 'onMouseOver',
  205. click: 'onClick',
  206. focus: 'onFocus',
  207. blur: 'onBlur',
  208. }),
  209. registerTriggerEvent: (triggerEventSet: Record<string, any>) => {
  210. this.setState({ triggerEventSet });
  211. },
  212. unregisterTriggerEvent: () => {},
  213. registerPortalEvent: (portalEventSet: Record<string, any>) => {
  214. this.setState({ portalEventSet });
  215. },
  216. unregisterPortalEvent: () => {},
  217. getTriggerBounding: () => {
  218. // eslint-disable-next-line
  219. // It may be a React component or an html element
  220. // There is no guarantee that triggerE l.current can get the real dom, so call findDOMNode to ensure that you can get the real dom
  221. let triggerDOM = this.triggerEl.current;
  222. if (!isHTMLElement(this.triggerEl.current)) {
  223. const realDomNode = ReactDOM.findDOMNode(this.triggerEl.current as React.ReactInstance);
  224. (this.triggerEl as any).current = realDomNode;
  225. triggerDOM = realDomNode;
  226. }
  227. return triggerDOM && (triggerDOM as Element).getBoundingClientRect();
  228. },
  229. // Gets the outer size of the specified container
  230. getPopupContainerRect: () => {
  231. const container = this.getPopupContainer();
  232. let rect: PopupContainerDOMRect = null;
  233. if (container && isHTMLElement(container)) {
  234. const boundingRect: DOMRectLikeType = convertDOMRectToObject(container.getBoundingClientRect());
  235. rect = {
  236. ...boundingRect,
  237. scrollLeft: container.scrollLeft,
  238. scrollTop: container.scrollTop,
  239. };
  240. }
  241. return rect;
  242. },
  243. containerIsBody: () => {
  244. const container = this.getPopupContainer();
  245. return container === document.body;
  246. },
  247. containerIsRelative: () => {
  248. const container = this.getPopupContainer();
  249. const computedStyle = window.getComputedStyle(container);
  250. return computedStyle.getPropertyValue('position') === 'relative';
  251. },
  252. containerIsRelativeOrAbsolute: () => ['relative', 'absolute'].includes(this.containerPosition),
  253. // Get the size of the pop-up layer
  254. getWrapperBounding: () => {
  255. const el = this.containerEl && this.containerEl.current;
  256. return el && (el as Element).getBoundingClientRect();
  257. },
  258. getDocumentElementBounding: () => document.documentElement.getBoundingClientRect(),
  259. setPosition: ({ position, ...style }: { position: Position }) => {
  260. this.setState(
  261. { containerStyle: { ...this.state.containerStyle, ...style }, placement: position },
  262. () => {
  263. this.eventManager.emit('positionUpdated');
  264. }
  265. );
  266. },
  267. updatePlacementAttr: (placement: Position) => {
  268. this.setState({ placement });
  269. },
  270. togglePortalVisible: (visible: boolean, cb: () => void) => {
  271. const willUpdateStates: Partial<TooltipState> = {};
  272. if (this.adapter.canMotion()) {
  273. willUpdateStates.transitionState = visible ? 'enter' : 'leave';
  274. willUpdateStates.visible = visible;
  275. } else {
  276. willUpdateStates.visible = visible;
  277. }
  278. this.setState(willUpdateStates as TooltipState, () => {
  279. cb();
  280. });
  281. },
  282. registerClickOutsideHandler: (cb: () => void) => {
  283. if (this.clickOutsideHandler) {
  284. this.adapter.unregisterClickOutsideHandler();
  285. }
  286. this.clickOutsideHandler = (e: React.MouseEvent): any => {
  287. if (!this.mounted) {
  288. return false;
  289. }
  290. let el = this.triggerEl && this.triggerEl.current;
  291. let popupEl = this.containerEl && this.containerEl.current;
  292. el = ReactDOM.findDOMNode(el as React.ReactInstance);
  293. popupEl = ReactDOM.findDOMNode(popupEl as React.ReactInstance);
  294. if (
  295. (el && !(el as any).contains(e.target) && popupEl && !(popupEl as any).contains(e.target)) ||
  296. this.props.clickTriggerToHide
  297. ) {
  298. this.props.onClickOutSide(e);
  299. cb();
  300. }
  301. };
  302. document.addEventListener('click', this.clickOutsideHandler, false);
  303. },
  304. unregisterClickOutsideHandler: () => {
  305. if (this.clickOutsideHandler) {
  306. document.removeEventListener('click', this.clickOutsideHandler, false);
  307. this.clickOutsideHandler = null;
  308. }
  309. },
  310. registerResizeHandler: (cb: (e: any) => void) => {
  311. if (this.resizeHandler) {
  312. this.adapter.unregisterResizeHandler();
  313. }
  314. this.resizeHandler = throttle((e): any => {
  315. if (!this.mounted) {
  316. return false;
  317. }
  318. cb(e);
  319. }, 10);
  320. window.addEventListener('resize', this.resizeHandler, false);
  321. },
  322. unregisterResizeHandler: () => {
  323. if (this.resizeHandler) {
  324. window.removeEventListener('resize', this.resizeHandler, false);
  325. this.resizeHandler = null;
  326. }
  327. },
  328. notifyVisibleChange: (visible: boolean) => {
  329. this.props.onVisibleChange(visible);
  330. },
  331. registerScrollHandler: (rePositionCb: (arg: { x: number; y: number }) => void) => {
  332. if (this.scrollHandler) {
  333. this.adapter.unregisterScrollHandler();
  334. }
  335. this.scrollHandler = throttle((e): any => {
  336. if (!this.mounted) {
  337. return false;
  338. }
  339. let triggerDOM = this.triggerEl.current;
  340. if (!isHTMLElement(this.triggerEl.current)) {
  341. triggerDOM = ReactDOM.findDOMNode(this.triggerEl.current as React.ReactInstance);
  342. }
  343. const isRelativeScroll = e.target.contains(triggerDOM);
  344. if (isRelativeScroll) {
  345. const scrollPos = { x: e.target.scrollLeft, y: e.target.scrollTop };
  346. rePositionCb(scrollPos);
  347. }
  348. }, 10); // When it is greater than 16ms, it will be very obvious
  349. window.addEventListener('scroll', this.scrollHandler, true);
  350. },
  351. unregisterScrollHandler: () => {
  352. if (this.scrollHandler) {
  353. window.removeEventListener('scroll', this.scrollHandler, true);
  354. this.scrollHandler = null;
  355. }
  356. },
  357. canMotion: () => Boolean(this.props.motion),
  358. updateContainerPosition: () => {
  359. const container = this.getPopupContainer();
  360. if (container && isHTMLElement(container)) {
  361. // getComputedStyle need first parameter is Element type
  362. const computedStyle = window.getComputedStyle(container);
  363. const position = computedStyle.getPropertyValue('position');
  364. this.containerPosition = position;
  365. }
  366. },
  367. getContainerPosition: () => this.containerPosition,
  368. };
  369. }
  370. componentDidMount() {
  371. this.mounted = true;
  372. this.getPopupContainer = this.props.getPopupContainer || this.context.getPopupContainer || defaultGetContainer;
  373. this.foundation.init();
  374. }
  375. componentWillUnmount() {
  376. this.mounted = false;
  377. this.foundation.destroy();
  378. }
  379. isSpecial = (elem: React.ReactNode | HTMLElement | any) => {
  380. if (isHTMLElement(elem)) {
  381. return Boolean(elem.disabled);
  382. } else if (isValidElement(elem)) {
  383. const disabled = get(elem, 'props.disabled');
  384. if (disabled) {
  385. return strings.STATUS_DISABLED;
  386. }
  387. const loading = get(elem, 'props.loading');
  388. /* Only judge the loading state of the Button, and no longer judge other components */
  389. const isButton = !isEmpty(elem)
  390. && !isEmpty(elem.type)
  391. && (elem.type as any).name === 'Button'
  392. || (elem.type as any).name === 'IconButton';
  393. if (loading && isButton) {
  394. return strings.STATUS_LOADING;
  395. }
  396. }
  397. return false;
  398. };
  399. willEnter = () => {
  400. this.foundation.calcPosition();
  401. /**
  402. * Dangerous: remove setState in motion fix #1379
  403. * because togglePortalVisible callback function will use visible state to notifyVisibleChange
  404. * if visible state is old value, then notifyVisibleChange function will not be called
  405. * we should ensure that after calling togglePortalVisible, callback function can get right visible value
  406. */
  407. // this.setState({ visible: true });
  408. };
  409. didLeave = () => {
  410. this.adapter.unregisterClickOutsideHandler();
  411. this.adapter.unregisterScrollHandler();
  412. this.adapter.unregisterResizeHandler();
  413. this.adapter.removePortal();
  414. };
  415. /** for transition - end */
  416. rePosition() {
  417. return this.foundation.calcPosition();
  418. }
  419. componentDidUpdate(prevProps: TooltipProps, prevState: TooltipState) {
  420. warning(
  421. this.props.mouseLeaveDelay < this.props.mouseEnterDelay,
  422. "[Semi Tooltip] 'mouseLeaveDelay' cannot be less than 'mouseEnterDelay', which may cause the dropdown layer to not be hidden."
  423. );
  424. if (prevProps.visible !== this.props.visible) {
  425. this.props.visible ? this.foundation.delayShow() : this.foundation.delayHide();
  426. }
  427. if (prevProps.rePosKey !== this.props.rePosKey) {
  428. this.rePosition();
  429. }
  430. }
  431. renderIcon = () => {
  432. const { placement } = this.state;
  433. const { showArrow, prefixCls, style } = this.props;
  434. let icon = null;
  435. const triangleCls = classNames([`${prefixCls}-icon-arrow`]);
  436. const bgColor = get(style, 'backgroundColor');
  437. const iconComponent = placement.includes('left') || placement.includes('right') ?
  438. <TriangleArrowVertical /> :
  439. <TriangleArrow />;
  440. if (showArrow) {
  441. if (isValidElement(showArrow)) {
  442. icon = showArrow;
  443. } else {
  444. icon = React.cloneElement(iconComponent, { className: triangleCls, style: { color: bgColor, fill: 'currentColor' } });
  445. }
  446. }
  447. return icon;
  448. };
  449. handlePortalInnerClick = (e: React.MouseEvent) => {
  450. if (this.props.clickToHide) {
  451. this.foundation.hide();
  452. }
  453. if (this.props.stopPropagation) {
  454. stopPropagation(e);
  455. }
  456. };
  457. renderPortal = () => {
  458. const { containerStyle = {}, visible, portalEventSet, placement, transitionState } = this.state;
  459. const { prefixCls, content, showArrow, style, motion, zIndex } = this.props;
  460. const { className: propClassName } = this.props;
  461. const direction = this.context.direction;
  462. const className = classNames(propClassName, {
  463. [`${prefixCls}-wrapper`]: true,
  464. [`${prefixCls}-wrapper-show`]: visible,
  465. [`${prefixCls}-with-arrow`]: Boolean(showArrow),
  466. [`${prefixCls}-rtl`]: direction === 'rtl',
  467. });
  468. const icon = this.renderIcon();
  469. const portalInnerStyle = omit(containerStyle, motion ? ['transformOrigin'] : undefined);
  470. const transformOrigin = get(containerStyle, 'transformOrigin');
  471. const inner = motion ? (
  472. <TooltipTransition position={placement} willEnter={this.willEnter} didLeave={this.didLeave} motion={motion}>
  473. {
  474. transitionState === 'enter' ?
  475. ({ animateCls, animateStyle, animateEvents }) => (
  476. <div
  477. className={classNames(className, animateCls)}
  478. style={{
  479. visibility: 'visible',
  480. ...animateStyle,
  481. transformOrigin,
  482. ...style,
  483. }}
  484. {...portalEventSet}
  485. {...animateEvents}
  486. x-placement={placement}
  487. >
  488. {content}
  489. {icon}
  490. </div>
  491. ) :
  492. null
  493. }
  494. </TooltipTransition>
  495. ) : (
  496. <div className={className} {...portalEventSet} x-placement={placement} style={style}>
  497. {content}
  498. {icon}
  499. </div>
  500. );
  501. return (
  502. <Portal getPopupContainer={this.props.getPopupContainer} style={{ zIndex }}>
  503. <div
  504. className={`${BASE_CLASS_PREFIX}-portal-inner`}
  505. style={portalInnerStyle}
  506. ref={this.setContainerEl}
  507. onClick={this.handlePortalInnerClick}
  508. >
  509. {inner}
  510. </div>
  511. </Portal>
  512. );
  513. };
  514. wrapSpan = (elem: React.ReactNode | React.ReactElement) => {
  515. const { wrapperClassName } = this.props;
  516. const display = get(elem, 'props.style.display');
  517. const block = get(elem, 'props.block');
  518. const style: React.CSSProperties = {
  519. display: 'inline-block',
  520. };
  521. if (block || blockDisplays.includes(display)) {
  522. style.width = '100%';
  523. }
  524. return <span className={wrapperClassName} style={style}>{elem}</span>;
  525. };
  526. mergeEvents = (rawEvents: Record<string, any>, events: Record<string, any>) => {
  527. const mergedEvents = {};
  528. each(events, (handler: any, key) => {
  529. if (typeof handler === 'function') {
  530. mergedEvents[key] = (...args: any[]) => {
  531. handler(...args);
  532. if (rawEvents && typeof rawEvents[key] === 'function') {
  533. rawEvents[key](...args);
  534. }
  535. };
  536. }
  537. });
  538. return mergedEvents;
  539. };
  540. render() {
  541. const { isInsert, triggerEventSet } = this.state;
  542. const { wrapWhenSpecial } = this.props;
  543. let { children } = this.props;
  544. const childrenStyle = { ...get(children, 'props.style') };
  545. const extraStyle: React.CSSProperties = {};
  546. if (wrapWhenSpecial) {
  547. const isSpecial = this.isSpecial(children);
  548. if (isSpecial) {
  549. childrenStyle.pointerEvents = 'none';
  550. if (isSpecial === strings.STATUS_DISABLED) {
  551. extraStyle.cursor = 'not-allowed';
  552. }
  553. children = cloneElement(children as React.ReactElement, { style: childrenStyle });
  554. children = this.wrapSpan(children);
  555. this.isWrapped = true;
  556. } else if (!isValidElement(children)) {
  557. children = this.wrapSpan(children);
  558. this.isWrapped = true;
  559. }
  560. }
  561. // The incoming children is a single valid element, otherwise wrap a layer with span
  562. const newChild = React.cloneElement(children as React.ReactElement, {
  563. ...(children as React.ReactElement).props,
  564. ...this.mergeEvents((children as React.ReactElement).props, triggerEventSet),
  565. style: {
  566. ...get(children, 'props.style'),
  567. ...extraStyle,
  568. },
  569. className: classNames(
  570. get(children, 'props.className')
  571. // `${prefixCls}-trigger`
  572. ),
  573. // to maintain refs with callback
  574. ref: (node: React.ReactNode) => {
  575. // Keep your own reference
  576. (this.triggerEl as any).current = node;
  577. // Call the original ref, if any
  578. const { ref } = children as React.ComponentPropsWithRef<any>;
  579. // this.log('tooltip render() - get ref', ref);
  580. if (typeof ref === 'function') {
  581. ref(node);
  582. } else if (ref && typeof ref === 'object') {
  583. ref.current = node;
  584. }
  585. },
  586. });
  587. // If you do not add a layer of div, in order to bind the events and className in the tooltip, you need to cloneElement children, but this time it may overwrite the children's original ref reference
  588. // So if the user adds ref to the content, you need to use callback ref: https://github.com/facebook/react/issues/8873
  589. return (
  590. <React.Fragment>
  591. {isInsert ? this.renderPortal() : null}
  592. {newChild}
  593. </React.Fragment>
  594. );
  595. }
  596. }
  597. export { Position };