foundation.ts 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933
  1. /* eslint-disable no-param-reassign */
  2. /* eslint-disable prefer-destructuring, max-lines-per-function, one-var, max-len, @typescript-eslint/restrict-plus-operands */
  3. /* argus-disable unPkgSensitiveInfo */
  4. import { get, isEmpty } from 'lodash';
  5. import { DOMRectLikeType } from '../utils/dom';
  6. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  7. import { ArrayElement } from '../utils/type';
  8. import { strings } from './constants';
  9. const REGS = {
  10. TOP: /top/i,
  11. RIGHT: /right/i,
  12. BOTTOM: /bottom/i,
  13. LEFT: /left/i,
  14. };
  15. const defaultRect = {
  16. left: 0,
  17. top: 0,
  18. height: 0,
  19. width: 0,
  20. scrollLeft: 0,
  21. scrollTop: 0,
  22. };
  23. export interface TooltipAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  24. registerPortalEvent(portalEventSet: any): void;
  25. unregisterPortalEvent(): void;
  26. registerResizeHandler(onResize: () => void): void;
  27. unregisterResizeHandler(onResize?: () => void): void;
  28. on(arg0: string, arg1: () => void): void;
  29. notifyVisibleChange(isVisible: any): void;
  30. getPopupContainerRect(): PopupContainerDOMRect;
  31. containerIsBody(): boolean;
  32. off(arg0: string): void;
  33. canMotion(): boolean;
  34. registerScrollHandler(arg: () => Record<string, any>): void;
  35. unregisterScrollHandler(): void;
  36. insertPortal(...args: any[]): void;
  37. removePortal(...args: any[]): void;
  38. getEventName(): {
  39. mouseEnter: string;
  40. mouseLeave: string;
  41. mouseOut: string;
  42. mouseOver: string;
  43. click: string;
  44. focus: string;
  45. blur: string;
  46. keydown: string;
  47. };
  48. registerTriggerEvent(...args: any[]): void;
  49. getTriggerBounding(...args: any[]): DOMRect;
  50. getWrapperBounding(...args: any[]): DOMRect;
  51. setPosition(...args: any[]): void;
  52. togglePortalVisible(...args: any[]): void;
  53. registerClickOutsideHandler(...args: any[]): void;
  54. unregisterClickOutsideHandler(...args: any[]): void;
  55. unregisterTriggerEvent(): void;
  56. containerIsRelative(): boolean;
  57. containerIsRelativeOrAbsolute(): boolean;
  58. getDocumentElementBounding(): DOMRect;
  59. updateContainerPosition(): void;
  60. updatePlacementAttr(placement: Position): void;
  61. getContainerPosition(): string;
  62. getFocusableElements(node: any): any[];
  63. getActiveElement(): any;
  64. getContainer(): any;
  65. setInitialFocus(): void;
  66. notifyEscKeydown(event: any): void;
  67. getTriggerNode(): any;
  68. setId(): void;
  69. }
  70. export type Position = ArrayElement<typeof strings.POSITION_SET>;
  71. export interface PopupContainerDOMRect extends DOMRectLikeType {
  72. scrollLeft?: number;
  73. scrollTop?: number;
  74. }
  75. export default class Tooltip<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<TooltipAdapter<P, S>, P, S> {
  76. _timer: ReturnType<typeof setTimeout>;
  77. _mounted: boolean;
  78. constructor(adapter: TooltipAdapter<P, S>) {
  79. super({ ...adapter });
  80. this._timer = null;
  81. }
  82. init() {
  83. const { wrapperId } = this.getProps();
  84. this._mounted = true;
  85. this._bindEvent();
  86. this._shouldShow();
  87. this._initContainerPosition();
  88. if (!wrapperId) {
  89. this._adapter.setId();
  90. }
  91. }
  92. destroy() {
  93. this._mounted = false;
  94. this._unBindEvent();
  95. }
  96. _bindEvent() {
  97. const trigger = this.getProp('trigger'); // get trigger type
  98. const { triggerEventSet, portalEventSet } = this._generateEvent(trigger);
  99. this._bindTriggerEvent(triggerEventSet);
  100. this._bindPortalEvent(portalEventSet);
  101. this._bindResizeEvent();
  102. }
  103. _unBindEvent() {
  104. this._unBindTriggerEvent();
  105. this._unBindPortalEvent();
  106. this._unBindResizeEvent();
  107. this._unBindScrollEvent();
  108. }
  109. _bindTriggerEvent(triggerEventSet: Record<string, any>) {
  110. this._adapter.registerTriggerEvent(triggerEventSet);
  111. }
  112. _unBindTriggerEvent() {
  113. this._adapter.unregisterTriggerEvent();
  114. }
  115. _bindPortalEvent(portalEventSet: Record<string, any>) {
  116. this._adapter.registerPortalEvent(portalEventSet);
  117. }
  118. _unBindPortalEvent() {
  119. this._adapter.unregisterPortalEvent();
  120. }
  121. _bindResizeEvent() {
  122. this._adapter.registerResizeHandler(this.onResize);
  123. }
  124. _unBindResizeEvent() {
  125. this._adapter.unregisterResizeHandler(this.onResize);
  126. }
  127. _reversePos(position = '', isVertical = false) {
  128. if (isVertical) {
  129. if (REGS.TOP.test(position)) {
  130. return position.replace('top', 'bottom').replace('Top', 'Bottom');
  131. } else if (REGS.BOTTOM.test(position)) {
  132. return position.replace('bottom', 'top').replace('Bottom', 'Top');
  133. }
  134. } else if (REGS.LEFT.test(position)) {
  135. return position.replace('left', 'right').replace('Left', 'Right');
  136. } else if (REGS.RIGHT.test(position)) {
  137. return position.replace('right', 'left').replace('Right', 'Left');
  138. }
  139. return position;
  140. }
  141. clearDelayTimer() {
  142. if (this._timer) {
  143. clearTimeout(this._timer);
  144. this._timer = null;
  145. }
  146. }
  147. _generateEvent(types: ArrayElement<typeof strings.TRIGGER_SET>) {
  148. const eventNames = this._adapter.getEventName();
  149. const triggerEventSet = {
  150. // bind esc keydown on trigger for a11y
  151. [eventNames.keydown]: (event) => {
  152. this._handleTriggerKeydown(event);
  153. },
  154. };
  155. let portalEventSet = {};
  156. switch (types) {
  157. case 'focus':
  158. triggerEventSet[eventNames.focus] = () => {
  159. this.delayShow();
  160. };
  161. triggerEventSet[eventNames.blur] = () => {
  162. this.delayHide();
  163. };
  164. portalEventSet = triggerEventSet;
  165. break;
  166. case 'click':
  167. triggerEventSet[eventNames.click] = () => {
  168. // this.delayShow();
  169. this.show();
  170. };
  171. portalEventSet = {};
  172. // Click outside needs special treatment, can not be directly tied to the trigger Element, need to be bound to the document
  173. break;
  174. case 'hover':
  175. triggerEventSet[eventNames.mouseEnter] = () => {
  176. // console.log(e);
  177. this.setCache('isClickToHide', false);
  178. this.delayShow();
  179. // this.show('trigger');
  180. };
  181. triggerEventSet[eventNames.mouseLeave] = () => {
  182. // console.log(e);
  183. this.delayHide();
  184. // this.hide('trigger');
  185. };
  186. // bind focus to hover trigger for a11y
  187. triggerEventSet[eventNames.focus] = () => {
  188. this.delayShow();
  189. };
  190. triggerEventSet[eventNames.blur] = () => {
  191. this.delayHide();
  192. };
  193. portalEventSet = { ...triggerEventSet };
  194. if (this.getProp('clickToHide')) {
  195. portalEventSet[eventNames.click] = () => {
  196. this.setCache('isClickToHide', true);
  197. this.hide();
  198. };
  199. portalEventSet[eventNames.mouseEnter] = () => {
  200. if (this.getCache('isClickToHide')) {
  201. return;
  202. }
  203. this.delayShow();
  204. };
  205. }
  206. break;
  207. case 'custom':
  208. // when trigger type is 'custom', no need to bind eventHandler
  209. // show/hide completely depend on props.visible which change by user
  210. break;
  211. default:
  212. break;
  213. }
  214. return { triggerEventSet, portalEventSet };
  215. }
  216. onResize = () => {
  217. // this.log('resize');
  218. // rePosition when window resize
  219. this.calcPosition();
  220. };
  221. _shouldShow() {
  222. const visible = this.getProp('visible');
  223. if (visible) {
  224. this.show();
  225. } else {
  226. // this.hide();
  227. }
  228. }
  229. delayShow = () => {
  230. const mouseEnterDelay: number = this.getProp('mouseEnterDelay');
  231. this.clearDelayTimer();
  232. if (mouseEnterDelay > 0) {
  233. this._timer = setTimeout(() => {
  234. this.show();
  235. this.clearDelayTimer();
  236. }, mouseEnterDelay);
  237. } else {
  238. this.show();
  239. }
  240. };
  241. show = () => {
  242. const content = this.getProp('content');
  243. const trigger = this.getProp('trigger');
  244. const clickTriggerToHide = this.getProp('clickTriggerToHide');
  245. this.clearDelayTimer();
  246. /**
  247. * If you emit an event in setState callback, you need to place the event listener function before setState to execute.
  248. * This is to avoid event registration being executed later than setState callback when setState is executed in setTimeout.
  249. * internal-issues:1402#note_38969412
  250. */
  251. this._adapter.on('portalInserted', () => {
  252. this.calcPosition();
  253. });
  254. this._adapter.on('positionUpdated', () => {
  255. this._togglePortalVisible(true);
  256. });
  257. const position = this.calcPosition(null, null, null, false);
  258. this._adapter.insertPortal(content, position);
  259. if (trigger === 'custom') {
  260. // eslint-disable-next-line
  261. this._adapter.registerClickOutsideHandler(() => {});
  262. }
  263. /**
  264. * trigger类型是click时,仅当portal被插入显示后,才绑定clickOutsideHandler
  265. * 因为handler需要绑定在document上。如果在constructor阶段绑定
  266. * 当一个页面中有多个容器实例时,一次click会触发多个容器的handler
  267. *
  268. * When the trigger type is click, clickOutsideHandler is bound only after the portal is inserted and displayed
  269. * Because the handler needs to be bound to the document. If you bind during the constructor phase
  270. * When there are multiple container instances in a page, one click triggers the handler of multiple containers
  271. */
  272. if (trigger === 'click' || clickTriggerToHide) {
  273. this._adapter.registerClickOutsideHandler(this.hide);
  274. }
  275. this._bindScrollEvent();
  276. this._bindResizeEvent();
  277. };
  278. _togglePortalVisible(isVisible: boolean) {
  279. const nowVisible = this.getState('visible');
  280. if (nowVisible !== isVisible) {
  281. this._adapter.togglePortalVisible(isVisible, () => {
  282. if (isVisible) {
  283. this._adapter.setInitialFocus();
  284. }
  285. this._adapter.notifyVisibleChange(isVisible);
  286. });
  287. }
  288. }
  289. _roundPixel(pixel: number) {
  290. if (typeof pixel === 'number') {
  291. return Math.round(pixel);
  292. }
  293. return pixel;
  294. }
  295. calcTransformOrigin(position: Position, triggerRect: DOMRect, translateX: number, translateY: number) {
  296. // eslint-disable-next-line
  297. if (position && triggerRect && translateX != null && translateY != null) {
  298. if (this.getProp('transformFromCenter')) {
  299. if (['topLeft', 'bottomLeft'].includes(position)) {
  300. return `${this._roundPixel(triggerRect.width / 2)}px ${-translateY * 100}%`;
  301. }
  302. if (['topRight', 'bottomRight'].includes(position)) {
  303. return `calc(100% - ${this._roundPixel(triggerRect.width / 2)}px) ${-translateY * 100}%`;
  304. }
  305. if (['leftTop', 'rightTop'].includes(position)) {
  306. return `${-translateX * 100}% ${this._roundPixel(triggerRect.height / 2)}px`;
  307. }
  308. if (['leftBottom', 'rightBottom'].includes(position)) {
  309. return `${-translateX * 100}% calc(100% - ${this._roundPixel(triggerRect.height / 2)}px)`;
  310. }
  311. }
  312. return `${-translateX * 100}% ${-translateY * 100}%`;
  313. }
  314. return null;
  315. }
  316. calcPosStyle(triggerRect: DOMRect, wrapperRect: DOMRect, containerRect: PopupContainerDOMRect, position?: Position, spacing?: number) {
  317. triggerRect = (isEmpty(triggerRect) ? triggerRect : this._adapter.getTriggerBounding()) || { ...defaultRect as any };
  318. containerRect = (isEmpty(containerRect) ? containerRect : this._adapter.getPopupContainerRect()) || {
  319. ...defaultRect,
  320. };
  321. wrapperRect = (isEmpty(wrapperRect) ? wrapperRect : this._adapter.getWrapperBounding()) || { ...defaultRect as any };
  322. // eslint-disable-next-line
  323. position = position != null ? position : this.getProp('position');
  324. // eslint-disable-next-line
  325. const SPACING = spacing != null ? spacing : this.getProp('spacing');
  326. const { arrowPointAtCenter, showArrow, arrowBounding } = this.getProps();
  327. const pointAtCenter = showArrow && arrowPointAtCenter;
  328. const horizontalArrowWidth = get(arrowBounding, 'width', 24);
  329. const verticalArrowHeight = get(arrowBounding, 'width', 24);
  330. const arrowOffsetY = get(arrowBounding, 'offsetY', 0);
  331. const positionOffsetX = 6;
  332. const positionOffsetY = 6;
  333. // You must use left/top when rendering, using right/bottom does not render the element position correctly
  334. // Use left/top + translate to achieve tooltip positioning perfectly without knowing the size of the tooltip expansion layer
  335. let left;
  336. let top;
  337. let translateX = 0; // Container x-direction translation distance
  338. let translateY = 0; // Container y-direction translation distance
  339. const middleX = triggerRect.left + triggerRect.width / 2;
  340. const middleY = triggerRect.top + triggerRect.height / 2;
  341. const offsetXWithArrow = positionOffsetX + horizontalArrowWidth / 2;
  342. const offsetYWithArrow = positionOffsetY + verticalArrowHeight / 2;
  343. switch (position) {
  344. case 'top':
  345. left = middleX;
  346. top = triggerRect.top - SPACING;
  347. translateX = -0.5;
  348. translateY = -1;
  349. break;
  350. case 'topLeft':
  351. left = pointAtCenter ? middleX - offsetXWithArrow : triggerRect.left;
  352. top = triggerRect.top - SPACING;
  353. translateY = -1;
  354. break;
  355. case 'topRight':
  356. left = pointAtCenter ? middleX + offsetXWithArrow : triggerRect.right;
  357. top = triggerRect.top - SPACING;
  358. translateY = -1;
  359. translateX = -1;
  360. break;
  361. case 'left':
  362. left = triggerRect.left - SPACING;
  363. top = middleY;
  364. translateX = -1;
  365. translateY = -0.5;
  366. break;
  367. case 'leftTop':
  368. left = triggerRect.left - SPACING;
  369. top = pointAtCenter ? middleY - offsetYWithArrow : triggerRect.top;
  370. translateX = -1;
  371. break;
  372. case 'leftBottom':
  373. left = triggerRect.left - SPACING;
  374. top = pointAtCenter ? middleY + offsetYWithArrow : triggerRect.bottom;
  375. translateX = -1;
  376. translateY = -1;
  377. break;
  378. case 'bottom':
  379. left = middleX;
  380. top = triggerRect.top + triggerRect.height + SPACING;
  381. translateX = -0.5;
  382. break;
  383. case 'bottomLeft':
  384. left = pointAtCenter ? middleX - offsetXWithArrow : triggerRect.left;
  385. top = triggerRect.bottom + SPACING;
  386. break;
  387. case 'bottomRight':
  388. left = pointAtCenter ? middleX + offsetXWithArrow : triggerRect.right;
  389. top = triggerRect.bottom + SPACING;
  390. translateX = -1;
  391. break;
  392. case 'right':
  393. left = triggerRect.right + SPACING;
  394. top = middleY;
  395. translateY = -0.5;
  396. break;
  397. case 'rightTop':
  398. left = triggerRect.right + SPACING;
  399. top = pointAtCenter ? middleY - offsetYWithArrow : triggerRect.top;
  400. break;
  401. case 'rightBottom':
  402. left = triggerRect.right + SPACING;
  403. top = pointAtCenter ? middleY + offsetYWithArrow : triggerRect.bottom;
  404. translateY = -1;
  405. break;
  406. case 'leftTopOver':
  407. left = triggerRect.left - SPACING;
  408. top = triggerRect.top - SPACING;
  409. break;
  410. case 'rightTopOver':
  411. left = triggerRect.right + SPACING;
  412. top = triggerRect.top - SPACING;
  413. translateX = -1;
  414. break;
  415. case 'leftBottomOver':
  416. left = triggerRect.left - SPACING;
  417. top = triggerRect.bottom + SPACING;
  418. translateY = -1;
  419. break;
  420. case 'rightBottomOver':
  421. left = triggerRect.right + SPACING;
  422. top = triggerRect.bottom + SPACING;
  423. translateX = -1;
  424. translateY = -1;
  425. break;
  426. default:
  427. break;
  428. }
  429. const transformOrigin = this.calcTransformOrigin(position, triggerRect, translateX, translateY); // Transform origin
  430. const _containerIsBody = this._adapter.containerIsBody();
  431. // Calculate container positioning relative to window
  432. left = left - containerRect.left;
  433. top = top - containerRect.top;
  434. /**
  435. * container为body时,如果position不为relative或absolute,这时trigger计算出的top/left会根据html定位(initial containing block)
  436. * 此时如果body有margin,则计算出的位置相对于body会有问题 fix issue #1368
  437. *
  438. * When container is body, if position is not relative or absolute, then the top/left calculated by trigger will be positioned according to html
  439. * At this time, if the body has a margin, the calculated position will have a problem relative to the body fix issue #1368
  440. */
  441. if (_containerIsBody && !this._adapter.containerIsRelativeOrAbsolute()) {
  442. const documentEleRect = this._adapter.getDocumentElementBounding();
  443. // Represents the left of the body relative to html
  444. left += containerRect.left - documentEleRect.left;
  445. // Represents the top of the body relative to html
  446. top += containerRect.top - documentEleRect.top;
  447. }
  448. // ContainerRect.scrollLeft to solve the inner scrolling of the container
  449. left = _containerIsBody ? left : left + containerRect.scrollLeft;
  450. top = _containerIsBody ? top : top + containerRect.scrollTop;
  451. const triggerHeight = triggerRect.height;
  452. if (
  453. this.getProp('showArrow') &&
  454. !arrowPointAtCenter &&
  455. triggerHeight <= (verticalArrowHeight / 2 + arrowOffsetY) * 2
  456. ) {
  457. const offsetY = triggerHeight / 2 - (arrowOffsetY + verticalArrowHeight / 2);
  458. if ((position.includes('Top') || position.includes('Bottom')) && !position.includes('Over')) {
  459. top = position.includes('Top') ? top + offsetY : top - offsetY;
  460. }
  461. }
  462. // The left/top value here must be rounded, otherwise it will cause the small triangle to shake
  463. const style: Record<string, string | number> = {
  464. left: this._roundPixel(left),
  465. top: this._roundPixel(top),
  466. };
  467. let transform = '';
  468. // eslint-disable-next-line
  469. if (translateX != null) {
  470. transform += `translateX(${translateX * 100}%) `;
  471. Object.defineProperty(style, 'translateX', {
  472. enumerable: false,
  473. value: translateX,
  474. });
  475. }
  476. // eslint-disable-next-line
  477. if (translateY != null) {
  478. transform += `translateY(${translateY * 100}%) `;
  479. Object.defineProperty(style, 'translateY', {
  480. enumerable: false,
  481. value: translateY,
  482. });
  483. }
  484. // eslint-disable-next-line
  485. if (transformOrigin != null) {
  486. style.transformOrigin = transformOrigin;
  487. }
  488. if (transform) {
  489. style.transform = transform;
  490. }
  491. return style;
  492. }
  493. /**
  494. * 耦合的东西比较多,稍微罗列一下:
  495. *
  496. * - 根据 trigger 和 wrapper 的 boundingClient 计算当前的 left、top、transform-origin
  497. * - 根据当前的 position 和 wrapper 的 boundingClient 决定是否需要自动调整位置
  498. * - 根据当前的 position、trigger 的 boundingClient 以及 motion.handleStyle 调整当前的 style
  499. *
  500. * There are many coupling things, a little list:
  501. *
  502. * - calculate the current left, top, and transfer-origin according to the boundingClient of trigger and wrapper
  503. * - decide whether to automatically adjust the position according to the current position and the boundingClient of wrapper
  504. * - adjust the current style according to the current position, the boundingClient of trigger and motion.handle Style
  505. */
  506. calcPosition = (triggerRect?: DOMRect, wrapperRect?: DOMRect, containerRect?: PopupContainerDOMRect, shouldUpdatePos = true) => {
  507. triggerRect = (isEmpty(triggerRect) ? this._adapter.getTriggerBounding() : triggerRect) || { ...defaultRect as any };
  508. containerRect = (isEmpty(containerRect) ? this._adapter.getPopupContainerRect() : containerRect) || {
  509. ...defaultRect,
  510. };
  511. wrapperRect = (isEmpty(wrapperRect) ? this._adapter.getWrapperBounding() : wrapperRect) || { ...defaultRect as any };
  512. // console.log('containerRect: ', containerRect, 'triggerRect: ', triggerRect, 'wrapperRect: ', wrapperRect);
  513. let style = this.calcPosStyle(triggerRect, wrapperRect, containerRect);
  514. let position = this.getProp('position');
  515. if (this.getProp('autoAdjustOverflow')) {
  516. // console.log('style: ', style, '\ntriggerRect: ', triggerRect, '\nwrapperRect: ', wrapperRect);
  517. const adjustedPos = this.adjustPosIfNeed(position, style, triggerRect, wrapperRect, containerRect);
  518. if (position !== adjustedPos) {
  519. position = adjustedPos;
  520. style = this.calcPosStyle(triggerRect, wrapperRect, containerRect, position);
  521. }
  522. }
  523. if (shouldUpdatePos && this._mounted) {
  524. // this._adapter.updatePlacementAttr(style.position);
  525. this._adapter.setPosition({ ...style, position });
  526. }
  527. return style;
  528. };
  529. isLR(position = '') {
  530. return position.indexOf('left') === 0 || position.indexOf('right') === 0;
  531. }
  532. isTB(position = '') {
  533. return position.indexOf('top') === 0 || position.indexOf('bottom') === 0;
  534. }
  535. // place the dom correctly
  536. adjustPosIfNeed(position: Position | string, style: Record<string, any>, triggerRect: DOMRect, wrapperRect: DOMRect, containerRect: PopupContainerDOMRect) {
  537. const { innerWidth, innerHeight } = window;
  538. const { spacing } = this.getProps();
  539. if (wrapperRect.width > 0 && wrapperRect.height > 0) {
  540. // let clientLeft = left + translateX * wrapperRect.width - containerRect.scrollLeft;
  541. // let clientTop = top + translateY * wrapperRect.height - containerRect.scrollTop;
  542. // if (this._adapter.containerIsBody() || this._adapter.containerIsRelative()) {
  543. // clientLeft += containerRect.left;
  544. // clientTop += containerRect.top;
  545. // }
  546. // const clientRight = clientLeft + wrapperRect.width;
  547. // const clientBottom = clientTop + wrapperRect.height;
  548. // The relative position of the elements on the screen
  549. // https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/tooltip-pic.svg
  550. const clientLeft = triggerRect.left;
  551. const clientRight = triggerRect.right;
  552. const clientTop = triggerRect.top;
  553. const clientBottom = triggerRect.bottom;
  554. const restClientLeft = innerWidth - clientLeft;
  555. const restClientTop = innerHeight - clientTop;
  556. const restClientRight = innerWidth - clientRight;
  557. const restClientBottom = innerHeight - clientBottom;
  558. const widthIsBigger = wrapperRect.width > triggerRect.width;
  559. const heightIsBigger = wrapperRect.height > triggerRect.height;
  560. // The wrapperR ect.top|bottom equivalent cannot be directly used here for comparison, which is easy to cause jitter
  561. const shouldReverseTop = clientTop < wrapperRect.height + spacing && restClientBottom > wrapperRect.height + spacing;
  562. const shouldReverseLeft = clientLeft < wrapperRect.width + spacing && restClientRight > wrapperRect.width + spacing;
  563. const shouldReverseBottom = restClientBottom < wrapperRect.height + spacing && clientTop > wrapperRect.height + spacing;
  564. const shouldReverseRight = restClientRight < wrapperRect.width + spacing && clientLeft > wrapperRect.width + spacing;
  565. const shouldReverseTopOver = restClientTop < wrapperRect.height + spacing && clientBottom > wrapperRect.height + spacing;
  566. const shouldReverseBottomOver = clientBottom < wrapperRect.height + spacing && restClientTop > wrapperRect.height + spacing;
  567. const shouldReverseTopSide = restClientTop < wrapperRect.height && clientBottom > wrapperRect.height;
  568. const shouldReverseBottomSide = clientBottom < wrapperRect.height && restClientTop > wrapperRect.height;
  569. const shouldReverseLeftSide = restClientLeft < wrapperRect.width && clientRight > wrapperRect.width;
  570. const shouldReverseRightSide = clientRight < wrapperRect.width && restClientLeft > wrapperRect.width;
  571. const shouldReverseLeftOver = restClientLeft < wrapperRect.width && clientRight > wrapperRect.width;
  572. const shouldReverseRightOver = clientRight < wrapperRect.width && restClientLeft > wrapperRect.width;
  573. switch (position) {
  574. case 'top':
  575. if (shouldReverseTop) {
  576. position = this._reversePos(position, true);
  577. }
  578. break;
  579. case 'topLeft':
  580. if (shouldReverseTop) {
  581. position = this._reversePos(position, true);
  582. }
  583. if (shouldReverseLeftSide && widthIsBigger) {
  584. position = this._reversePos(position);
  585. }
  586. break;
  587. case 'topRight':
  588. if (shouldReverseTop) {
  589. position = this._reversePos(position, true);
  590. }
  591. if (shouldReverseRightSide && widthIsBigger) {
  592. position = this._reversePos(position);
  593. }
  594. break;
  595. case 'left':
  596. if (shouldReverseLeft) {
  597. position = this._reversePos(position);
  598. }
  599. break;
  600. case 'leftTop':
  601. if (shouldReverseLeft) {
  602. position = this._reversePos(position);
  603. }
  604. if (shouldReverseTopSide && heightIsBigger) {
  605. position = this._reversePos(position, true);
  606. }
  607. break;
  608. case 'leftBottom':
  609. if (shouldReverseLeft) {
  610. position = this._reversePos(position);
  611. }
  612. if (shouldReverseBottomSide && heightIsBigger) {
  613. position = this._reversePos(position, true);
  614. }
  615. break;
  616. case 'bottom':
  617. if (shouldReverseBottom) {
  618. position = this._reversePos(position, true);
  619. }
  620. break;
  621. case 'bottomLeft':
  622. if (shouldReverseBottom) {
  623. position = this._reversePos(position, true);
  624. }
  625. if (shouldReverseLeftSide && widthIsBigger) {
  626. position = this._reversePos(position);
  627. }
  628. break;
  629. case 'bottomRight':
  630. if (shouldReverseBottom) {
  631. position = this._reversePos(position, true);
  632. }
  633. if (shouldReverseRightSide && widthIsBigger) {
  634. position = this._reversePos(position);
  635. }
  636. break;
  637. case 'right':
  638. if (shouldReverseRight) {
  639. position = this._reversePos(position);
  640. }
  641. break;
  642. case 'rightTop':
  643. if (shouldReverseRight) {
  644. position = this._reversePos(position);
  645. }
  646. if (shouldReverseTopSide && heightIsBigger) {
  647. position = this._reversePos(position, true);
  648. }
  649. break;
  650. case 'rightBottom':
  651. if (shouldReverseRight) {
  652. position = this._reversePos(position);
  653. }
  654. if (shouldReverseBottomSide && heightIsBigger) {
  655. position = this._reversePos(position, true);
  656. }
  657. break;
  658. case 'leftTopOver':
  659. if (shouldReverseTopOver) {
  660. position = this._reversePos(position, true);
  661. }
  662. if (shouldReverseLeftOver) {
  663. position = this._reversePos(position);
  664. }
  665. break;
  666. case 'leftBottomOver':
  667. if (shouldReverseBottomOver) {
  668. position = this._reversePos(position, true);
  669. }
  670. if (shouldReverseLeftOver) {
  671. position = this._reversePos(position);
  672. }
  673. break;
  674. case 'rightTopOver':
  675. if (shouldReverseTopOver) {
  676. position = this._reversePos(position, true);
  677. }
  678. if (shouldReverseRightOver) {
  679. position = this._reversePos(position);
  680. }
  681. break;
  682. case 'rightBottomOver':
  683. if (shouldReverseBottomOver) {
  684. position = this._reversePos(position, true);
  685. }
  686. if (shouldReverseRightOver) {
  687. position = this._reversePos(position);
  688. }
  689. break;
  690. default:
  691. break;
  692. }
  693. }
  694. return position;
  695. }
  696. delayHide = () => {
  697. const mouseLeaveDelay = this.getProp('mouseLeaveDelay');
  698. this.clearDelayTimer();
  699. if (mouseLeaveDelay > 0) {
  700. this._timer = setTimeout(() => {
  701. // console.log('delayHide for ', mouseLeaveDelay, ' ms, ', ...args);
  702. this.hide();
  703. this.clearDelayTimer();
  704. }, mouseLeaveDelay);
  705. } else {
  706. this.hide();
  707. }
  708. };
  709. hide = () => {
  710. this.clearDelayTimer();
  711. this._togglePortalVisible(false);
  712. this._adapter.off('portalInserted');
  713. this._adapter.off('positionUpdated');
  714. if (!this._adapter.canMotion()) {
  715. this._adapter.removePortal();
  716. // When the portal is removed, the global click outside event binding is also removed
  717. this._adapter.unregisterClickOutsideHandler();
  718. this._unBindScrollEvent();
  719. this._unBindResizeEvent();
  720. }
  721. };
  722. _bindScrollEvent() {
  723. this._adapter.registerScrollHandler(() => this.calcPosition());
  724. // Capture scroll events on the window to determine whether the current scrolling area (e.target) will affect the positioning of the pop-up layer relative to the viewport when scrolling
  725. // (By determining whether the e.target contains the triggerDom of the current tooltip) If so, the pop-up layer will also be affected and needs to be repositioned
  726. }
  727. _unBindScrollEvent() {
  728. this._adapter.unregisterScrollHandler();
  729. }
  730. _initContainerPosition() {
  731. this._adapter.updateContainerPosition();
  732. }
  733. handleContainerKeydown = (event: any) => {
  734. const { guardFocus, closeOnEsc } = this.getProps();
  735. switch (event && event.key) {
  736. case "Escape":
  737. closeOnEsc && this._handleEscKeyDown(event);
  738. break;
  739. case "Tab":
  740. if (guardFocus) {
  741. const container = this._adapter.getContainer();
  742. const focusableElements = this._adapter.getFocusableElements(container);
  743. const focusableNum = focusableElements.length;
  744. if (focusableNum) {
  745. // Shift + Tab will move focus backward
  746. if (event.shiftKey) {
  747. this._handleContainerShiftTabKeyDown(focusableElements, event);
  748. } else {
  749. this._handleContainerTabKeyDown(focusableElements, event);
  750. }
  751. }
  752. }
  753. break;
  754. default:
  755. break;
  756. }
  757. }
  758. _handleTriggerKeydown(event: any) {
  759. const { closeOnEsc } = this.getProps();
  760. const container = this._adapter.getContainer();
  761. const focusableElements = this._adapter.getFocusableElements(container);
  762. const focusableNum = focusableElements.length;
  763. switch (event && event.key) {
  764. case "Escape":
  765. closeOnEsc && this._handleEscKeyDown(event);
  766. break;
  767. case "ArrowUp":
  768. focusableNum && this._handleTriggerArrowUpKeydown(focusableElements, event);
  769. break;
  770. case "ArrowDown":
  771. focusableNum && this._handleTriggerArrowDownKeydown(focusableElements, event);
  772. break;
  773. default:
  774. break;
  775. }
  776. }
  777. /**
  778. * focus trigger
  779. *
  780. * when trigger is 'focus' or 'hover', onFocus is bind to show popup
  781. * if we focus trigger, popup will show again
  782. *
  783. * 如果 trigger 是 focus 或者 hover,则它绑定了 onFocus,这里我们如果重新 focus 的话,popup 会再次打开
  784. * 因此 returnFocusOnClose 只支持 click trigger
  785. */
  786. _focusTrigger() {
  787. const { trigger, returnFocusOnClose } = this.getProps();
  788. if (returnFocusOnClose && trigger === 'click') {
  789. const triggerNode = this._adapter.getTriggerNode();
  790. if (triggerNode && 'focus' in triggerNode) {
  791. triggerNode.focus();
  792. }
  793. }
  794. }
  795. _handleEscKeyDown(event: any) {
  796. const { trigger } = this.getProps();
  797. if (trigger !== 'custom') {
  798. this.hide();
  799. this._focusTrigger();
  800. }
  801. this._adapter.notifyEscKeydown(event);
  802. }
  803. _handleContainerTabKeyDown(focusableElements: any[], event: any) {
  804. const activeElement = this._adapter.getActiveElement();
  805. const isLastCurrentFocus = focusableElements[focusableElements.length - 1] === activeElement;
  806. if (isLastCurrentFocus) {
  807. focusableElements[0].focus();
  808. event.preventDefault(); // prevent browser default tab move behavior
  809. }
  810. }
  811. _handleContainerShiftTabKeyDown(focusableElements: any[], event: any) {
  812. const activeElement = this._adapter.getActiveElement();
  813. const isFirstCurrentFocus = focusableElements[0] === activeElement;
  814. if (isFirstCurrentFocus) {
  815. focusableElements[focusableElements.length - 1].focus();
  816. event.preventDefault(); // prevent browser default tab move behavior
  817. }
  818. }
  819. _handleTriggerArrowDownKeydown(focusableElements: any[], event: any) {
  820. focusableElements[0].focus();
  821. event.preventDefault(); // prevent browser default scroll behavior
  822. }
  823. _handleTriggerArrowUpKeydown(focusableElements: any[], event: any) {
  824. focusableElements[focusableElements.length - 1].focus();
  825. event.preventDefault(); // prevent browser default scroll behavior
  826. }
  827. }