foundation.ts 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192
  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. import { handlePrevent } from '../utils/a11y';
  10. const REGS = {
  11. TOP: /top/i,
  12. RIGHT: /right/i,
  13. BOTTOM: /bottom/i,
  14. LEFT: /left/i,
  15. };
  16. const defaultRect = {
  17. left: 0,
  18. top: 0,
  19. height: 0,
  20. width: 0,
  21. scrollLeft: 0,
  22. scrollTop: 0,
  23. };
  24. export interface TooltipAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  25. registerPortalEvent(portalEventSet: any): 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. setDisplayNone: (displayNone: boolean, cb?: () => void) => void;
  39. getEventName(): {
  40. mouseEnter: string;
  41. mouseLeave: string;
  42. mouseOut: string;
  43. mouseOver: string;
  44. click: string;
  45. focus: string;
  46. blur: string;
  47. keydown: string
  48. };
  49. registerTriggerEvent(...args: any[]): void;
  50. getTriggerBounding(...args: any[]): DOMRect;
  51. getWrapperBounding(...args: any[]): DOMRect;
  52. setPosition(...args: any[]): void;
  53. togglePortalVisible(...args: any[]): void;
  54. registerClickOutsideHandler(...args: any[]): void;
  55. unregisterClickOutsideHandler(...args: any[]): 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._adapter.unregisterClickOutsideHandler();
  105. this.unBindResizeEvent();
  106. this.unBindScrollEvent();
  107. }
  108. _bindTriggerEvent(triggerEventSet: Record<string, any>) {
  109. this._adapter.registerTriggerEvent(triggerEventSet);
  110. }
  111. _bindPortalEvent(portalEventSet: Record<string, any>) {
  112. this._adapter.registerPortalEvent(portalEventSet);
  113. }
  114. _bindResizeEvent() {
  115. this._adapter.registerResizeHandler(this.onResize);
  116. }
  117. unBindResizeEvent() {
  118. this._adapter.unregisterResizeHandler(this.onResize);
  119. }
  120. removePortal = () => {
  121. this._adapter.removePortal();
  122. }
  123. setDisplayNone: (displayNone: boolean, cb?: () => void) => void = (displayNone, cb) => {
  124. this._adapter.setDisplayNone(displayNone, cb);
  125. }
  126. _adjustPos(position = '', isVertical = false, adjustType = 'reverse', concatPos?: any) {
  127. switch (adjustType) {
  128. case 'reverse':
  129. return this._reversePos(position, isVertical);
  130. case 'expand':
  131. // only happens when position is top/bottom/left/right
  132. return this._expandPos(position, concatPos);
  133. case 'reduce':
  134. // only happens when position other than top/bottom/left/right
  135. return this._reducePos(position);
  136. default:
  137. return this._reversePos(position, isVertical);
  138. }
  139. }
  140. _reversePos(position = '', isVertical = false) {
  141. if (isVertical) {
  142. if (REGS.TOP.test(position)) {
  143. return position.replace('top', 'bottom').replace('Top', 'Bottom');
  144. } else if (REGS.BOTTOM.test(position)) {
  145. return position.replace('bottom', 'top').replace('Bottom', 'Top');
  146. }
  147. } else if (REGS.LEFT.test(position)) {
  148. return position.replace('left', 'right').replace('Left', 'Right');
  149. } else if (REGS.RIGHT.test(position)) {
  150. return position.replace('right', 'left').replace('Right', 'Left');
  151. }
  152. return position;
  153. }
  154. _expandPos(position = '', concatPos: string) {
  155. return position.concat(concatPos);
  156. }
  157. _reducePos(position = '') {
  158. // if cur position consists of two directions, remove the last position
  159. const found = ['Top', 'Bottom', 'Left', 'Right'].find(pos => position.endsWith(pos));
  160. return found ? position.replace(found, ''): position;
  161. }
  162. clearDelayTimer() {
  163. if (this._timer) {
  164. clearTimeout(this._timer);
  165. this._timer = null;
  166. }
  167. }
  168. _generateEvent(types: ArrayElement<typeof strings.TRIGGER_SET>) {
  169. const eventNames = this._adapter.getEventName();
  170. const triggerEventSet = {
  171. // bind esc keydown on trigger for a11y
  172. [eventNames.keydown]: (event) => {
  173. this._handleTriggerKeydown(event);
  174. },
  175. };
  176. let portalEventSet = {};
  177. switch (types) {
  178. case 'focus':
  179. triggerEventSet[eventNames.focus] = () => {
  180. this.delayShow();
  181. };
  182. triggerEventSet[eventNames.blur] = () => {
  183. this.delayHide();
  184. };
  185. portalEventSet = triggerEventSet;
  186. break;
  187. case 'click':
  188. triggerEventSet[eventNames.click] = () => {
  189. // this.delayShow();
  190. this.show();
  191. };
  192. portalEventSet = {};
  193. // Click outside needs special treatment, can not be directly tied to the trigger Element, need to be bound to the document
  194. break;
  195. case 'hover':
  196. triggerEventSet[eventNames.mouseEnter] = () => {
  197. // console.log(e);
  198. this.setCache('isClickToHide', false);
  199. this.delayShow();
  200. // this.show('trigger');
  201. };
  202. triggerEventSet[eventNames.mouseLeave] = () => {
  203. // console.log(e);
  204. this.delayHide();
  205. // this.hide('trigger');
  206. };
  207. // bind focus to hover trigger for a11y
  208. triggerEventSet[eventNames.focus] = () => {
  209. const { disableFocusListener } = this.getProps();
  210. !disableFocusListener && this.delayShow();
  211. };
  212. triggerEventSet[eventNames.blur] = () => {
  213. const { disableFocusListener } = this.getProps();
  214. !disableFocusListener && this.delayHide();
  215. };
  216. portalEventSet = { ...triggerEventSet };
  217. if (this.getProp('clickToHide')) {
  218. portalEventSet[eventNames.click] = () => {
  219. this.setCache('isClickToHide', true);
  220. this.hide();
  221. };
  222. portalEventSet[eventNames.mouseEnter] = () => {
  223. if (this.getCache('isClickToHide')) {
  224. return;
  225. }
  226. this.delayShow();
  227. };
  228. }
  229. break;
  230. case 'custom':
  231. // when trigger type is 'custom', no need to bind eventHandler
  232. // show/hide completely depend on props.visible which change by user
  233. break;
  234. default:
  235. break;
  236. }
  237. return { triggerEventSet, portalEventSet };
  238. }
  239. onResize = () => {
  240. // this.log('resize');
  241. // rePosition when window resize
  242. this.calcPosition();
  243. };
  244. _shouldShow() {
  245. const visible = this.getProp('visible');
  246. if (visible) {
  247. this.show();
  248. } else {
  249. // this.hide();
  250. }
  251. }
  252. delayShow = () => {
  253. const mouseEnterDelay: number = this.getProp('mouseEnterDelay');
  254. this.clearDelayTimer();
  255. if (mouseEnterDelay > 0) {
  256. this._timer = setTimeout(() => {
  257. this.show();
  258. this.clearDelayTimer();
  259. }, mouseEnterDelay);
  260. } else {
  261. this.show();
  262. }
  263. };
  264. show = () => {
  265. const content = this.getProp('content');
  266. const trigger = this.getProp('trigger');
  267. const clickTriggerToHide = this.getProp('clickTriggerToHide');
  268. const { visible, displayNone } = this.getStates();
  269. if (displayNone) {
  270. this.setDisplayNone(false);
  271. }
  272. if (visible) {
  273. return ;
  274. }
  275. this.clearDelayTimer();
  276. /**
  277. * If you emit an event in setState callback, you need to place the event listener function before setState to execute.
  278. * This is to avoid event registration being executed later than setState callback when setState is executed in setTimeout.
  279. * internal-issues:1402#note_38969412
  280. */
  281. this._adapter.on('portalInserted', () => {
  282. this.calcPosition();
  283. });
  284. this._adapter.on('positionUpdated', () => {
  285. this._togglePortalVisible(true);
  286. });
  287. this._adapter.insertPortal(content, { left: -9999, top: -9999 }); // offscreen rendering
  288. if (trigger === 'custom') {
  289. // eslint-disable-next-line
  290. this._adapter.registerClickOutsideHandler(() => {});
  291. }
  292. /**
  293. * trigger类型是click时,仅当portal被插入显示后,才绑定clickOutsideHandler
  294. * 因为handler需要绑定在document上。如果在constructor阶段绑定
  295. * 当一个页面中有多个容器实例时,一次click会触发多个容器的handler
  296. *
  297. * When the trigger type is click, clickOutsideHandler is bound only after the portal is inserted and displayed
  298. * Because the handler needs to be bound to the document. If you bind during the constructor phase
  299. * When there are multiple container instances in a page, one click triggers the handler of multiple containers
  300. */
  301. if (trigger === 'click' || clickTriggerToHide) {
  302. this._adapter.registerClickOutsideHandler(this.hide);
  303. }
  304. this._bindScrollEvent();
  305. this._bindResizeEvent();
  306. };
  307. _togglePortalVisible(isVisible: boolean) {
  308. const nowVisible = this.getState('visible');
  309. if (nowVisible !== isVisible) {
  310. this._adapter.togglePortalVisible(isVisible, () => {
  311. if (isVisible) {
  312. this._adapter.setInitialFocus();
  313. }
  314. this._adapter.notifyVisibleChange(isVisible);
  315. });
  316. }
  317. }
  318. _roundPixel(pixel: number) {
  319. if (typeof pixel === 'number') {
  320. return Math.round(pixel);
  321. }
  322. return pixel;
  323. }
  324. calcTransformOrigin(position: Position, triggerRect: DOMRect, translateX: number, translateY: number) {
  325. // eslint-disable-next-line
  326. if (position && triggerRect && translateX != null && translateY != null) {
  327. if (this.getProp('transformFromCenter')) {
  328. if (['topLeft', 'bottomLeft'].includes(position)) {
  329. return `${this._roundPixel(triggerRect.width / 2)}px ${-translateY * 100}%`;
  330. }
  331. if (['topRight', 'bottomRight'].includes(position)) {
  332. return `calc(100% - ${this._roundPixel(triggerRect.width / 2)}px) ${-translateY * 100}%`;
  333. }
  334. if (['leftTop', 'rightTop'].includes(position)) {
  335. return `${-translateX * 100}% ${this._roundPixel(triggerRect.height / 2)}px`;
  336. }
  337. if (['leftBottom', 'rightBottom'].includes(position)) {
  338. return `${-translateX * 100}% calc(100% - ${this._roundPixel(triggerRect.height / 2)}px)`;
  339. }
  340. }
  341. return `${-translateX * 100}% ${-translateY * 100}%`;
  342. }
  343. return null;
  344. }
  345. calcPosStyle(props: {triggerRect: DOMRect; wrapperRect: DOMRect; containerRect: PopupContainerDOMRect; position?: Position; spacing?: number; isOverFlow?: [boolean, boolean]}) {
  346. const { spacing, isOverFlow } = props;
  347. const { innerWidth } = window;
  348. const triggerRect = (isEmpty(props.triggerRect) ? props.triggerRect : this._adapter.getTriggerBounding()) || { ...defaultRect as any };
  349. const containerRect = (isEmpty(props.containerRect) ? props.containerRect : this._adapter.getPopupContainerRect()) || {
  350. ...defaultRect,
  351. };
  352. const wrapperRect = (isEmpty(props.wrapperRect) ? props.wrapperRect : this._adapter.getWrapperBounding()) || { ...defaultRect as any };
  353. // eslint-disable-next-line
  354. const position = props.position != null ? props.position : this.getProp('position');
  355. // eslint-disable-next-line
  356. const SPACING = spacing != null ? spacing : this.getProp('spacing');
  357. const { arrowPointAtCenter, showArrow, arrowBounding } = this.getProps();
  358. const pointAtCenter = showArrow && arrowPointAtCenter;
  359. const horizontalArrowWidth = get(arrowBounding, 'width', 24);
  360. const verticalArrowHeight = get(arrowBounding, 'width', 24);
  361. const arrowOffsetY = get(arrowBounding, 'offsetY', 0);
  362. const positionOffsetX = 6;
  363. const positionOffsetY = 6;
  364. // You must use left/top when rendering, using right/bottom does not render the element position correctly
  365. // Use left/top + translate to achieve tooltip positioning perfectly without knowing the size of the tooltip expansion layer
  366. let left;
  367. let top;
  368. let translateX = 0; // Container x-direction translation distance
  369. let translateY = 0; // Container y-direction translation distance
  370. const middleX = triggerRect.left + triggerRect.width / 2;
  371. const middleY = triggerRect.top + triggerRect.height / 2;
  372. const offsetXWithArrow = positionOffsetX + horizontalArrowWidth / 2;
  373. const offsetYWithArrow = positionOffsetY + verticalArrowHeight / 2;
  374. const heightDifference = wrapperRect.height - containerRect.height;
  375. const widthDifference = wrapperRect.width - containerRect.width;
  376. const offsetHeight = heightDifference > 0 ? heightDifference : 0;
  377. const offsetWidth = widthDifference > 0 ? widthDifference : 0;
  378. const isHeightOverFlow = isOverFlow && isOverFlow[0];
  379. const isWidthOverFlow = isOverFlow && isOverFlow[1];
  380. const isTriggerNearLeft = middleX - containerRect.left < containerRect.right - middleX;
  381. const isTriggerNearTop = middleY - containerRect.top < containerRect.bottom - middleY;
  382. const isWrapperWidthOverflow = wrapperRect.width > innerWidth;
  383. switch (position) {
  384. case 'top':
  385. // left = middleX;
  386. // top = triggerRect.top - SPACING;
  387. left = isWidthOverFlow ? (isTriggerNearLeft ? containerRect.left + wrapperRect.width / 2 : containerRect.right - wrapperRect.width / 2 + offsetWidth): middleX;
  388. top = isHeightOverFlow ? containerRect.bottom + offsetHeight : triggerRect.top - SPACING;
  389. translateX = -0.5;
  390. translateY = -1;
  391. break;
  392. case 'topLeft':
  393. // left = pointAtCenter ? middleX - offsetXWithArrow : triggerRect.left;
  394. // top = triggerRect.top - SPACING;
  395. left = isWidthOverFlow ? (isWrapperWidthOverflow ? containerRect.left : containerRect.right - wrapperRect.width ) : (pointAtCenter ? middleX - offsetXWithArrow : triggerRect.left);
  396. top = isHeightOverFlow ? containerRect.bottom + offsetHeight : triggerRect.top - SPACING;
  397. translateY = -1;
  398. break;
  399. case 'topRight':
  400. // left = pointAtCenter ? middleX + offsetXWithArrow : triggerRect.right;
  401. // top = triggerRect.top - SPACING;
  402. left = isWidthOverFlow ? containerRect.right + offsetWidth : (pointAtCenter ? middleX + offsetXWithArrow : triggerRect.right);
  403. top = isHeightOverFlow ? containerRect.bottom + offsetHeight : triggerRect.top - SPACING;
  404. translateY = -1;
  405. translateX = -1;
  406. break;
  407. case 'left':
  408. // left = triggerRect.left - SPACING;
  409. // top = middleY;
  410. // left = isWidthOverFlow? containerRect.right - SPACING : triggerRect.left - SPACING;
  411. left = isWidthOverFlow ? containerRect.right + offsetWidth - SPACING + offsetXWithArrow : triggerRect.left - SPACING;
  412. top = isHeightOverFlow ? (isTriggerNearTop ? containerRect.top + wrapperRect.height / 2 : containerRect.bottom - wrapperRect.height / 2 + offsetHeight): middleY;
  413. translateX = -1;
  414. translateY = -0.5;
  415. break;
  416. case 'leftTop':
  417. // left = triggerRect.left - SPACING;
  418. // top = pointAtCenter ? middleY - offsetYWithArrow : triggerRect.top;
  419. left = isWidthOverFlow ? containerRect.right + offsetWidth - SPACING + offsetXWithArrow : triggerRect.left - SPACING;
  420. top = isHeightOverFlow ? containerRect.top : (pointAtCenter ? middleY - offsetYWithArrow : triggerRect.top);
  421. translateX = -1;
  422. break;
  423. case 'leftBottom':
  424. // left = triggerRect.left - SPACING;
  425. // top = pointAtCenter ? middleY + offsetYWithArrow : triggerRect.bottom;
  426. left = isWidthOverFlow ? containerRect.right + offsetWidth - SPACING + offsetXWithArrow: triggerRect.left - SPACING;
  427. top = isHeightOverFlow ? containerRect.bottom + offsetHeight: (pointAtCenter ? middleY + offsetYWithArrow : triggerRect.bottom);
  428. translateX = -1;
  429. translateY = -1;
  430. break;
  431. case 'bottom':
  432. // left = middleX;
  433. // top = triggerRect.top + triggerRect.height + SPACING;
  434. left = isWidthOverFlow ? (isTriggerNearLeft ? containerRect.left + wrapperRect.width / 2 : containerRect.right - wrapperRect.width / 2 + offsetWidth): middleX;
  435. top = isHeightOverFlow ? containerRect.top + offsetYWithArrow - SPACING: triggerRect.top + triggerRect.height + SPACING;
  436. translateX = -0.5;
  437. break;
  438. case 'bottomLeft':
  439. // left = pointAtCenter ? middleX - offsetXWithArrow : triggerRect.left;
  440. // top = triggerRect.bottom + SPACING;
  441. left = isWidthOverFlow ? (isWrapperWidthOverflow ? containerRect.left : containerRect.right - wrapperRect.width ) : (pointAtCenter ? middleX - offsetXWithArrow : triggerRect.left);
  442. top = isHeightOverFlow ? containerRect.top + offsetYWithArrow - SPACING : triggerRect.top + triggerRect.height + SPACING;
  443. break;
  444. case 'bottomRight':
  445. // left = pointAtCenter ? middleX + offsetXWithArrow : triggerRect.right;
  446. // top = triggerRect.bottom + SPACING;
  447. left = isWidthOverFlow ? containerRect.right + offsetWidth : (pointAtCenter ? middleX + offsetXWithArrow : triggerRect.right);
  448. top = isHeightOverFlow ? containerRect.top + offsetYWithArrow - SPACING : triggerRect.top + triggerRect.height + SPACING;
  449. translateX = -1;
  450. break;
  451. case 'right':
  452. // left = triggerRect.right + SPACING;
  453. // top = middleY;
  454. left = isWidthOverFlow ? containerRect.left - SPACING + offsetXWithArrow : triggerRect.right + SPACING;
  455. top = isHeightOverFlow ? (isTriggerNearTop ? containerRect.top + wrapperRect.height / 2 : containerRect.bottom - wrapperRect.height / 2 + offsetHeight) : middleY;
  456. translateY = -0.5;
  457. break;
  458. case 'rightTop':
  459. // left = triggerRect.right + SPACING;
  460. // top = pointAtCenter ? middleY - offsetYWithArrow : triggerRect.top;
  461. left = isWidthOverFlow ? containerRect.left - SPACING + offsetXWithArrow : triggerRect.right + SPACING;
  462. top = isHeightOverFlow ? containerRect.top : (pointAtCenter ? middleY - offsetYWithArrow : triggerRect.top);
  463. break;
  464. case 'rightBottom':
  465. // left = triggerRect.right + SPACING;
  466. // top = pointAtCenter ? middleY + offsetYWithArrow : triggerRect.bottom;
  467. left = isWidthOverFlow ? containerRect.left - SPACING + offsetXWithArrow : triggerRect.right + SPACING;
  468. top = isHeightOverFlow ? containerRect.bottom + offsetHeight : (pointAtCenter ? middleY + offsetYWithArrow : triggerRect.bottom);
  469. translateY = -1;
  470. break;
  471. case 'leftTopOver':
  472. left = triggerRect.left - SPACING;
  473. top = triggerRect.top - SPACING;
  474. break;
  475. case 'rightTopOver':
  476. left = triggerRect.right + SPACING;
  477. top = triggerRect.top - SPACING;
  478. translateX = -1;
  479. break;
  480. case 'leftBottomOver':
  481. left = triggerRect.left - SPACING;
  482. top = triggerRect.bottom + SPACING;
  483. translateY = -1;
  484. break;
  485. case 'rightBottomOver':
  486. left = triggerRect.right + SPACING;
  487. top = triggerRect.bottom + SPACING;
  488. translateX = -1;
  489. translateY = -1;
  490. break;
  491. default:
  492. break;
  493. }
  494. const transformOrigin = this.calcTransformOrigin(position, triggerRect, translateX, translateY); // Transform origin
  495. const _containerIsBody = this._adapter.containerIsBody();
  496. // Calculate container positioning relative to window
  497. left = left - containerRect.left;
  498. top = top - containerRect.top;
  499. /**
  500. * container为body时,如果position不为relative或absolute,这时trigger计算出的top/left会根据html定位(initial containing block)
  501. * 此时如果body有margin,则计算出的位置相对于body会有问题 fix issue #1368
  502. *
  503. * When container is body, if position is not relative or absolute, then the top/left calculated by trigger will be positioned according to html
  504. * At this time, if the body has a margin, the calculated position will have a problem relative to the body fix issue #1368
  505. */
  506. if (_containerIsBody && !this._adapter.containerIsRelativeOrAbsolute()) {
  507. const documentEleRect = this._adapter.getDocumentElementBounding();
  508. // Represents the left of the body relative to html
  509. left += containerRect.left - documentEleRect.left;
  510. // Represents the top of the body relative to html
  511. top += containerRect.top - documentEleRect.top;
  512. }
  513. // ContainerRect.scrollLeft to solve the inner scrolling of the container
  514. left = _containerIsBody ? left : left + containerRect.scrollLeft;
  515. top = _containerIsBody ? top : top + containerRect.scrollTop;
  516. const triggerHeight = triggerRect.height;
  517. if (
  518. this.getProp('showArrow') &&
  519. !arrowPointAtCenter &&
  520. triggerHeight <= (verticalArrowHeight / 2 + arrowOffsetY) * 2
  521. ) {
  522. const offsetY = triggerHeight / 2 - (arrowOffsetY + verticalArrowHeight / 2);
  523. if ((position.includes('Top') || position.includes('Bottom')) && !position.includes('Over')) {
  524. top = position.includes('Top') ? top + offsetY : top - offsetY;
  525. }
  526. }
  527. // The left/top value here must be rounded, otherwise it will cause the small triangle to shake
  528. const style: Record<string, string | number> = {
  529. left: this._roundPixel(left),
  530. top: this._roundPixel(top),
  531. };
  532. let transform = '';
  533. // eslint-disable-next-line
  534. if (translateX != null) {
  535. transform += `translateX(${translateX * 100}%) `;
  536. Object.defineProperty(style, 'translateX', {
  537. enumerable: false,
  538. value: translateX,
  539. });
  540. }
  541. // eslint-disable-next-line
  542. if (translateY != null) {
  543. transform += `translateY(${translateY * 100}%) `;
  544. Object.defineProperty(style, 'translateY', {
  545. enumerable: false,
  546. value: translateY,
  547. });
  548. }
  549. // eslint-disable-next-line
  550. if (transformOrigin != null) {
  551. style.transformOrigin = transformOrigin;
  552. }
  553. if (transform) {
  554. style.transform = transform;
  555. }
  556. return style;
  557. }
  558. /**
  559. * 耦合的东西比较多,稍微罗列一下:
  560. *
  561. * - 根据 trigger 和 wrapper 的 boundingClient 计算当前的 left、top、transform-origin
  562. * - 根据当前的 position 和 wrapper 的 boundingClient 决定是否需要自动调整位置
  563. * - 根据当前的 position、trigger 的 boundingClient 以及 motion.handleStyle 调整当前的 style
  564. *
  565. * There are many coupling things, a little list:
  566. *
  567. * - calculate the current left, top, and transfer-origin according to the boundingClient of trigger and wrapper
  568. * - decide whether to automatically adjust the position according to the current position and the boundingClient of wrapper
  569. * - adjust the current style according to the current position, the boundingClient of trigger and motion.handle Style
  570. */
  571. calcPosition = (triggerRect?: DOMRect, wrapperRect?: DOMRect, containerRect?: PopupContainerDOMRect, shouldUpdatePos = true) => {
  572. triggerRect = (isEmpty(triggerRect) ? this._adapter.getTriggerBounding() : triggerRect) || { ...defaultRect as any };
  573. containerRect = (isEmpty(containerRect) ? this._adapter.getPopupContainerRect() : containerRect) || {
  574. ...defaultRect,
  575. };
  576. wrapperRect = (isEmpty(wrapperRect) ? this._adapter.getWrapperBounding() : wrapperRect) || { ...defaultRect as any };
  577. // console.log('containerRect: ', containerRect, 'triggerRect: ', triggerRect, 'wrapperRect: ', wrapperRect);
  578. let style = this.calcPosStyle({ triggerRect, wrapperRect, containerRect });
  579. let position = this.getProp('position');
  580. if (this.getProp('autoAdjustOverflow')) {
  581. // console.log('style: ', style, '\ntriggerRect: ', triggerRect, '\nwrapperRect: ', wrapperRect);
  582. const { position: adjustedPos, isHeightOverFlow, isWidthOverFlow } = this.adjustPosIfNeed(position, style, triggerRect, wrapperRect, containerRect);
  583. if (position !== adjustedPos || isHeightOverFlow || isWidthOverFlow) {
  584. position = adjustedPos;
  585. style = this.calcPosStyle({ triggerRect, wrapperRect, containerRect, position, spacing: null, isOverFlow: [ isHeightOverFlow, isWidthOverFlow ] });
  586. }
  587. }
  588. if (shouldUpdatePos && this._mounted) {
  589. // this._adapter.updatePlacementAttr(style.position);
  590. this._adapter.setPosition({ ...style, position });
  591. }
  592. return style;
  593. };
  594. isLR(position = '') {
  595. return position.includes('left') || position.includes('right');
  596. }
  597. isTB(position = '') {
  598. return position.includes('top') || position.includes('bottom');
  599. }
  600. isReverse(rowSpace: number, reverseSpace: number, size: number) {
  601. // 原空间不足,反向空间足够
  602. // Insufficient original space, enough reverse space
  603. return rowSpace < size && reverseSpace > size;
  604. }
  605. isOverFlow(rowSpace: number, reverseSpace: number, size: number) {
  606. // 原空间且反向空间都不足
  607. // The original space and the reverse space are not enough
  608. return rowSpace < size && reverseSpace < size;
  609. }
  610. isHalfOverFlow(posSpace: number, negSpace: number, size: number) {
  611. // 正半空间或者负半空间不足,即表示有遮挡,需要偏移
  612. // Insufficient positive half space or negative half space means that there is occlusion and needs to be offset
  613. return posSpace < size || negSpace < size;
  614. }
  615. isHalfAllEnough(posSpace: number, negSpace: number, size: number) {
  616. // 正半空间和负半空间都足够,即表示可以从 topLeft/topRight 变成 top
  617. // Both positive and negative half-spaces are sufficient, which means you can change from topLeft/topRight to top
  618. return posSpace >= size || negSpace >= size;
  619. }
  620. getReverse(viewOverFlow: boolean, containerOverFlow: boolean, shouldReverseView: boolean, shouldReverseContainer: boolean) {
  621. /**
  622. * 基于视口和容器一起判断,以下几种情况允许从原方向转到反方向,以判断是否应该由top->bottom为例子
  623. *
  624. * 1. 视口上下空间不足 且 容器上空间❌下空间✅
  625. * 2. 视口上空间❌下空间✅ 且 容器上下空间不足
  626. * 3. 视口上空间❌下空间✅ 且 容器上空间❌下空间✅
  627. *
  628. * Based on the judgment of the viewport and the container, the following situations are allowed to turn from the original direction to the opposite direction
  629. * to judge whether it should be top->bottom as an example
  630. * 1. There is insufficient space above and below the viewport and the space above the container ❌ the space below ✅
  631. * 2. The space above the viewport ❌ the space below ✅ and the space above and below the container is insufficient
  632. * 3. Viewport upper space ❌ lower space✅ and container upper space ❌ lower space✅
  633. */
  634. return (viewOverFlow && shouldReverseContainer) || (shouldReverseView && containerOverFlow) || (shouldReverseView && shouldReverseContainer);
  635. }
  636. // place the dom correctly
  637. adjustPosIfNeed(position: Position | string, style: Record<string, any>, triggerRect: DOMRect, wrapperRect: DOMRect, containerRect: PopupContainerDOMRect) {
  638. const { innerWidth, innerHeight } = window;
  639. const { spacing, margin } = this.getProps();
  640. const marginLeft = typeof margin === 'number' ? margin : margin.marginLeft;
  641. const marginTop = typeof margin === 'number' ? margin : margin.marginTop;
  642. const marginRight = typeof margin === 'number' ? margin : margin.marginRight;
  643. const marginBottom = typeof margin === 'number' ? margin : margin.marginBottom;
  644. let isHeightOverFlow = false;
  645. let isWidthOverFlow = false;
  646. if (wrapperRect.width > 0 && wrapperRect.height > 0) {
  647. // let clientLeft = left + translateX * wrapperRect.width - containerRect.scrollLeft;
  648. // let clientTop = top + translateY * wrapperRect.height - containerRect.scrollTop;
  649. // if (this._adapter.containerIsBody() || this._adapter.containerIsRelative()) {
  650. // clientLeft += containerRect.left;
  651. // clientTop += containerRect.top;
  652. // }
  653. // const clientRight = clientLeft + wrapperRect.width;
  654. // const clientBottom = clientTop + wrapperRect.height;
  655. // The relative position of the elements on the screen
  656. // https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/tooltip-pic.svg
  657. const clientLeft = triggerRect.left;
  658. const clientRight = triggerRect.right;
  659. const clientTop = triggerRect.top;
  660. const clientBottom = triggerRect.bottom;
  661. const restClientLeft = innerWidth - clientLeft;
  662. const restClientTop = innerHeight - clientTop;
  663. const restClientRight = innerWidth - clientRight;
  664. const restClientBottom = innerHeight - clientBottom;
  665. const widthIsBigger = wrapperRect.width > triggerRect.width;
  666. const heightIsBigger = wrapperRect.height > triggerRect.height;
  667. // The wrapperR ect.top|bottom equivalent cannot be directly used here for comparison, which is easy to cause jitter
  668. // 基于视口的微调判断
  669. // Fine-tuning judgment based on viewport
  670. const shouldViewReverseTop = clientTop - marginTop < wrapperRect.height + spacing && restClientBottom - marginBottom > wrapperRect.height + spacing;
  671. const shouldViewReverseLeft = clientLeft - marginLeft < wrapperRect.width + spacing && restClientRight - marginRight > wrapperRect.width + spacing;
  672. const shouldViewReverseBottom = restClientBottom - marginBottom < wrapperRect.height + spacing && clientTop - marginTop > wrapperRect.height + spacing;
  673. const shouldViewReverseRight = restClientRight - marginRight < wrapperRect.width + spacing && clientLeft - marginLeft > wrapperRect.width + spacing;
  674. const shouldViewReverseTopOver = restClientTop - marginBottom< wrapperRect.height + spacing && clientBottom - marginTop> wrapperRect.height + spacing;
  675. const shouldViewReverseBottomOver = clientBottom - marginTop < wrapperRect.height + spacing && restClientTop - marginBottom > wrapperRect.height + spacing;
  676. const shouldViewReverseTopSide = restClientTop < wrapperRect.height && clientBottom > wrapperRect.height;
  677. const shouldViewReverseBottomSide = clientBottom < wrapperRect.height && restClientTop > wrapperRect.height;
  678. const shouldViewReverseLeftSide = restClientLeft < wrapperRect.width && clientRight > wrapperRect.width;
  679. const shouldViewReverseRightSide = clientRight < wrapperRect.width && restClientLeft > wrapperRect.width;
  680. const shouldReverseTopOver = restClientTop < wrapperRect.height + spacing && clientBottom > wrapperRect.height + spacing;
  681. const shouldReverseBottomOver = clientBottom < wrapperRect.height + spacing && restClientTop > wrapperRect.height + spacing;
  682. const shouldReverseLeftOver = restClientLeft < wrapperRect.width && clientRight > wrapperRect.width;
  683. const shouldReverseRightOver = clientRight < wrapperRect.width && restClientLeft > wrapperRect.width;
  684. // 基于容器的微调判断
  685. // Fine-tuning judgment based on container
  686. const clientTopInContainer = clientTop - containerRect.top;
  687. const clientLeftInContainer = clientLeft - containerRect.left;
  688. const clientBottomInContainer = clientTopInContainer + triggerRect.height;
  689. const clientRightInContainer = clientLeftInContainer + triggerRect.width;
  690. const restClientBottomInContainer = containerRect.bottom - clientBottom;
  691. const restClientRightInContainer = containerRect.right - clientRight;
  692. const restClientTopInContainer = restClientBottomInContainer + triggerRect.height;
  693. const restClientLeftInContainer = restClientRightInContainer + triggerRect.width;
  694. // 当原空间不足,反向空间足够时,可以反向。
  695. // When the original space is insufficient and the reverse space is sufficient, the reverse can be performed.
  696. const shouldContainerReverseTop = this.isReverse(clientTopInContainer - marginTop, restClientBottomInContainer - marginBottom, wrapperRect.height + spacing);
  697. const shouldContainerReverseLeft = this.isReverse(clientLeftInContainer - marginLeft, restClientRightInContainer - marginRight, wrapperRect.width + spacing);
  698. const shouldContainerReverseBottom = this.isReverse(restClientBottomInContainer - marginBottom, clientTopInContainer - marginTop, wrapperRect.height + spacing);
  699. const shouldContainerReverseRight = this.isReverse(restClientRightInContainer - marginRight, clientLeftInContainer - marginLeft, wrapperRect.width + spacing);
  700. const shouldContainerReverseTopOver = this.isReverse(restClientTopInContainer - marginBottom, clientBottomInContainer - marginTop, wrapperRect.height + spacing);
  701. const shouldContainerReverseBottomOver = this.isReverse(clientBottomInContainer - marginTop, restClientTopInContainer - marginBottom, wrapperRect.height + spacing);
  702. const shouldContainerReverseTopSide = this.isReverse(restClientTopInContainer, clientBottomInContainer, wrapperRect.height);
  703. const shouldContainerReverseBottomSide = this.isReverse(clientBottomInContainer, restClientTopInContainer, wrapperRect.height);
  704. const shouldContainerReverseLeftSide = this.isReverse(restClientLeftInContainer, clientRightInContainer, wrapperRect.width);
  705. const shouldContainerReverseRightSide = this.isReverse(clientRightInContainer, restClientLeftInContainer, wrapperRect.width);
  706. const halfHeight = triggerRect.height / 2;
  707. const halfWidth = triggerRect.width / 2;
  708. // 视口, 原空间与反向空间是否都不足判断
  709. // Viewport, whether the original space and the reverse space are insufficient to judge
  710. const isViewYOverFlow = this.isOverFlow(clientTop - marginTop, restClientBottom - marginBottom, wrapperRect.height + spacing);
  711. const isViewXOverFlow = this.isOverFlow(clientLeft - marginLeft, restClientRight - marginRight, wrapperRect.width + spacing);
  712. const isViewYOverFlowSide = this.isOverFlow(clientBottom - marginTop, restClientTop - marginBottom, wrapperRect.height + spacing);
  713. const isViewXOverFlowSide = this.isOverFlow(clientRight - marginLeft, restClientLeft - marginRight, wrapperRect.width + spacing);
  714. const isViewYOverFlowSideHalf = this.isHalfOverFlow(clientBottom - halfHeight, restClientTop - halfHeight, wrapperRect.height / 2);
  715. const isViewXOverFlowSideHalf = this.isHalfOverFlow(clientRight - halfWidth, restClientLeft - halfWidth, wrapperRect.width / 2);
  716. const isViewYEnoughSideHalf = this.isHalfAllEnough(clientBottom - halfHeight, restClientTop - halfHeight, wrapperRect.height / 2);
  717. const isViewXEnoughSideHalf = this.isHalfAllEnough(clientRight - halfWidth, restClientLeft - halfWidth, wrapperRect.width / 2);
  718. // 容器, 原空间与反向空间是否都不足判断
  719. // container, whether the original space and the reverse space are insufficient to judge
  720. const isContainerYOverFlow = this.isOverFlow(clientTopInContainer - marginTop, restClientBottomInContainer - marginBottom, wrapperRect.height + spacing);
  721. const isContainerXOverFlow = this.isOverFlow(clientLeftInContainer - marginLeft, restClientRightInContainer - marginRight, wrapperRect.width + spacing);
  722. const isContainerYOverFlowSide = this.isOverFlow(clientBottomInContainer - marginTop, restClientTopInContainer - marginBottom, wrapperRect.height + spacing);
  723. const isContainerXOverFlowSide = this.isOverFlow(clientRightInContainer - marginLeft, restClientLeftInContainer - marginRight, wrapperRect.width + spacing);
  724. const isContainerYOverFlowSideHalf = this.isHalfOverFlow(clientBottomInContainer - halfHeight, restClientTopInContainer - halfHeight, wrapperRect.height / 2);
  725. const isContainerXOverFlowSideHalf = this.isHalfOverFlow(clientRightInContainer - halfWidth, restClientLeftInContainer - halfWidth, wrapperRect.width / 2);
  726. const isContainerYEnoughSideHalf = this.isHalfAllEnough(clientBottomInContainer - halfHeight, restClientTopInContainer - halfHeight, wrapperRect.height / 2);
  727. const isContainerXEnoughSideHalf = this.isHalfAllEnough(clientRightInContainer - halfWidth, restClientLeftInContainer - halfWidth, wrapperRect.width / 2);
  728. // 综合 viewport + container 判断微调,即视口 + 容器都放置不行时才能考虑位置调整
  729. // Comprehensive viewport + container judgment fine-tuning, that is, the position adjustment can only be considered when the viewport + container cannot be placed.
  730. const shouldReverseTop = this.getReverse(isViewYOverFlow, isContainerYOverFlow, shouldViewReverseTop, shouldContainerReverseTop);
  731. const shouldReverseLeft = this.getReverse(isViewXOverFlow, isContainerXOverFlow, shouldViewReverseLeft, shouldContainerReverseLeft);
  732. const shouldReverseBottom = this.getReverse(isViewYOverFlow, isContainerYOverFlow, shouldViewReverseBottom, shouldContainerReverseBottom);
  733. const shouldReverseRight = this.getReverse(isViewXOverFlow, isContainerXOverFlow, shouldViewReverseRight, shouldContainerReverseRight);
  734. // const shouldReverseTopOver = this.getReverse(isViewYOverFlowSide, isContainerYOverFlowSide, shouldViewReverseTopOver, shouldContainerReverseTopOver);
  735. // const shouldReverseBottomOver = this.getReverse(isViewYOverFlowSide, isContainerYOverFlowSide, shouldViewReverseBottomOver, shouldContainerReverseBottomOver);
  736. const shouldReverseTopSide = this.getReverse(isViewYOverFlowSide, isContainerYOverFlowSide, shouldViewReverseTopSide, shouldContainerReverseTopSide);
  737. const shouldReverseBottomSide = this.getReverse(isViewYOverFlowSide, isContainerYOverFlowSide, shouldViewReverseBottomSide, shouldContainerReverseBottomSide);
  738. const shouldReverseLeftSide = this.getReverse(isViewXOverFlowSide, isContainerXOverFlowSide, shouldViewReverseLeftSide, shouldContainerReverseLeftSide);
  739. const shouldReverseRightSide = this.getReverse(isViewXOverFlowSide, isContainerXOverFlowSide, shouldViewReverseRightSide, shouldContainerReverseRightSide);
  740. const isYOverFlowSideHalf = isViewYOverFlowSideHalf && isContainerYOverFlowSideHalf;
  741. const isXOverFlowSideHalf = isViewXOverFlowSideHalf && isContainerXOverFlowSideHalf;
  742. switch (position) {
  743. case 'top':
  744. if (shouldReverseTop) {
  745. position = this._adjustPos(position, true);
  746. }
  747. if (isXOverFlowSideHalf && (shouldReverseLeftSide || shouldReverseRightSide)) {
  748. position = this._adjustPos(position, true, 'expand', shouldReverseLeftSide ? 'Right' : 'Left');
  749. }
  750. break;
  751. case 'topLeft':
  752. if (shouldReverseTop) {
  753. position = this._adjustPos(position, true);
  754. }
  755. if (shouldReverseLeftSide && widthIsBigger) {
  756. position = this._adjustPos(position);
  757. }
  758. if (isWidthOverFlow && (isViewXEnoughSideHalf || isContainerXEnoughSideHalf)) {
  759. position = this._adjustPos(position, true, 'reduce');
  760. }
  761. break;
  762. case 'topRight':
  763. if (shouldReverseTop) {
  764. position = this._adjustPos(position, true);
  765. }
  766. if (shouldReverseRightSide && widthIsBigger) {
  767. position = this._adjustPos(position);
  768. }
  769. if (isWidthOverFlow && (isViewXEnoughSideHalf || isContainerXEnoughSideHalf)) {
  770. position = this._adjustPos(position, true, 'reduce');
  771. }
  772. break;
  773. case 'left':
  774. if (shouldReverseLeft) {
  775. position = this._adjustPos(position);
  776. }
  777. if (isYOverFlowSideHalf && (shouldReverseTopSide || shouldReverseBottomSide)) {
  778. position = this._adjustPos(position, false, 'expand', shouldReverseTopSide ? 'Bottom' : 'Top');
  779. }
  780. break;
  781. case 'leftTop':
  782. if (shouldReverseLeft) {
  783. position = this._adjustPos(position);
  784. }
  785. if (shouldReverseTopSide && heightIsBigger) {
  786. position = this._adjustPos(position, true);
  787. }
  788. if (isHeightOverFlow && (isViewYEnoughSideHalf || isContainerYEnoughSideHalf)) {
  789. position = this._adjustPos(position, false, 'reduce');
  790. }
  791. break;
  792. case 'leftBottom':
  793. if (shouldReverseLeft) {
  794. position = this._adjustPos(position);
  795. }
  796. if (shouldReverseBottomSide && heightIsBigger) {
  797. position = this._adjustPos(position, true);
  798. }
  799. if (isHeightOverFlow && (isViewYEnoughSideHalf || isContainerYEnoughSideHalf)) {
  800. position = this._adjustPos(position, false, 'reduce');
  801. }
  802. break;
  803. case 'bottom':
  804. if (shouldReverseBottom) {
  805. position = this._adjustPos(position, true);
  806. }
  807. if (isXOverFlowSideHalf && (shouldReverseLeftSide || shouldReverseRightSide)) {
  808. position = this._adjustPos(position, true, 'expand', shouldReverseLeftSide ? 'Right' : 'Left');
  809. }
  810. break;
  811. case 'bottomLeft':
  812. if (shouldReverseBottom) {
  813. position = this._adjustPos(position, true);
  814. }
  815. if (shouldReverseLeftSide && widthIsBigger) {
  816. position = this._adjustPos(position);
  817. }
  818. if (isWidthOverFlow && (isViewXEnoughSideHalf || isContainerXEnoughSideHalf)) {
  819. position = this._adjustPos(position, true, 'reduce');
  820. }
  821. break;
  822. case 'bottomRight':
  823. if (shouldReverseBottom) {
  824. position = this._adjustPos(position, true);
  825. }
  826. if (shouldReverseRightSide && widthIsBigger) {
  827. position = this._adjustPos(position);
  828. }
  829. if (isWidthOverFlow && (isViewXEnoughSideHalf || isContainerXEnoughSideHalf)) {
  830. position = this._adjustPos(position, true, 'reduce');
  831. }
  832. break;
  833. case 'right':
  834. if (shouldReverseRight) {
  835. position = this._adjustPos(position);
  836. }
  837. if (isYOverFlowSideHalf && (shouldReverseTopSide || shouldReverseBottomSide)) {
  838. position = this._adjustPos(position, false, 'expand', shouldReverseTopSide ? 'Bottom' : 'Top');
  839. }
  840. break;
  841. case 'rightTop':
  842. if (shouldReverseRight) {
  843. position = this._adjustPos(position);
  844. }
  845. if (shouldReverseTopSide && heightIsBigger) {
  846. position = this._adjustPos(position, true);
  847. }
  848. if (isHeightOverFlow && (isViewYEnoughSideHalf || isContainerYEnoughSideHalf)) {
  849. position = this._adjustPos(position, false, 'reduce');
  850. }
  851. break;
  852. case 'rightBottom':
  853. if (shouldReverseRight) {
  854. position = this._adjustPos(position);
  855. }
  856. if (shouldReverseBottomSide && heightIsBigger) {
  857. position = this._adjustPos(position, true);
  858. }
  859. if (isHeightOverFlow && (isViewYEnoughSideHalf || isContainerYEnoughSideHalf)) {
  860. position = this._adjustPos(position, false, 'reduce');
  861. }
  862. break;
  863. case 'leftTopOver':
  864. if (shouldReverseTopOver) {
  865. position = this._adjustPos(position, true);
  866. }
  867. if (shouldReverseLeftOver) {
  868. position = this._adjustPos(position);
  869. }
  870. break;
  871. case 'leftBottomOver':
  872. if (shouldReverseBottomOver) {
  873. position = this._adjustPos(position, true);
  874. }
  875. if (shouldReverseLeftOver) {
  876. position = this._adjustPos(position);
  877. }
  878. break;
  879. case 'rightTopOver':
  880. if (shouldReverseTopOver) {
  881. position = this._adjustPos(position, true);
  882. }
  883. if (shouldReverseRightOver) {
  884. position = this._adjustPos(position);
  885. }
  886. break;
  887. case 'rightBottomOver':
  888. if (shouldReverseBottomOver) {
  889. position = this._adjustPos(position, true);
  890. }
  891. if (shouldReverseRightOver) {
  892. position = this._adjustPos(position);
  893. }
  894. break;
  895. default:
  896. break;
  897. }
  898. // 判断溢出 Judgment overflow
  899. // 上下方向 top and bottom
  900. if (this.isTB(position)) {
  901. isHeightOverFlow = isViewYOverFlow && isContainerYOverFlow;
  902. // Related PR: https://github.com/DouyinFE/semi-design/pull/1297
  903. // If clientRight or restClientRight less than 0, means that the left and right parts of the trigger are blocked
  904. // Then the display of the wrapper will also be affected, make width overflow to offset the wrapper
  905. if (position === 'top' || position === 'bottom') {
  906. isWidthOverFlow = isViewXOverFlowSideHalf && isContainerXOverFlowSideHalf || (clientRight < 0 || restClientRight < 0);
  907. } else {
  908. isWidthOverFlow = isViewXOverFlowSide && isContainerXOverFlowSide || (clientRight < 0 || restClientRight < 0);
  909. }
  910. }
  911. // 左右方向 left and right
  912. if (this.isLR(position)) {
  913. isWidthOverFlow = isViewXOverFlow && isContainerXOverFlow;
  914. // If clientTop or restClientTop less than 0, means that the top and bottom parts of the trigger are blocked
  915. // Then the display of the wrapper will also be affected, make height overflow to offset the wrapper
  916. if (position === 'left' || position === 'right') {
  917. isHeightOverFlow = isViewYOverFlowSideHalf && isContainerYOverFlowSideHalf || (clientTop < 0 || restClientTop < 0);
  918. } else {
  919. isHeightOverFlow = isViewYOverFlowSide && isContainerYOverFlowSide || (clientTop < 0 || restClientTop < 0);
  920. }
  921. }
  922. }
  923. return { position, isHeightOverFlow, isWidthOverFlow };
  924. }
  925. delayHide = () => {
  926. const mouseLeaveDelay = this.getProp('mouseLeaveDelay');
  927. this.clearDelayTimer();
  928. if (mouseLeaveDelay > 0) {
  929. this._timer = setTimeout(() => {
  930. // console.log('delayHide for ', mouseLeaveDelay, ' ms, ', ...args);
  931. this.hide();
  932. this.clearDelayTimer();
  933. }, mouseLeaveDelay);
  934. } else {
  935. this.hide();
  936. }
  937. };
  938. hide = () => {
  939. this.clearDelayTimer();
  940. this._togglePortalVisible(false);
  941. this._adapter.off('portalInserted');
  942. this._adapter.off('positionUpdated');
  943. };
  944. _bindScrollEvent() {
  945. this._adapter.registerScrollHandler(() => this.calcPosition());
  946. // 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
  947. // (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
  948. }
  949. unBindScrollEvent() {
  950. this._adapter.unregisterScrollHandler();
  951. }
  952. _initContainerPosition() {
  953. this._adapter.updateContainerPosition();
  954. }
  955. handleContainerKeydown = (event: any) => {
  956. const { guardFocus, closeOnEsc } = this.getProps();
  957. switch (event && event.key) {
  958. case "Escape":
  959. closeOnEsc && this._handleEscKeyDown(event);
  960. break;
  961. case "Tab":
  962. if (guardFocus) {
  963. const container = this._adapter.getContainer();
  964. const focusableElements = this._adapter.getFocusableElements(container);
  965. const focusableNum = focusableElements.length;
  966. if (focusableNum) {
  967. // Shift + Tab will move focus backward
  968. if (event.shiftKey) {
  969. this._handleContainerShiftTabKeyDown(focusableElements, event);
  970. } else {
  971. this._handleContainerTabKeyDown(focusableElements, event);
  972. }
  973. }
  974. }
  975. break;
  976. default:
  977. break;
  978. }
  979. }
  980. _handleTriggerKeydown(event: any) {
  981. const { closeOnEsc, disableArrowKeyDown } = this.getProps();
  982. const container = this._adapter.getContainer();
  983. const focusableElements = this._adapter.getFocusableElements(container);
  984. const focusableNum = focusableElements.length;
  985. switch (event && event.key) {
  986. case "Escape":
  987. handlePrevent(event);
  988. closeOnEsc && this._handleEscKeyDown(event);
  989. break;
  990. case "ArrowUp":
  991. // when disableArrowKeyDown is true, disable tooltip's arrow keyboard event action
  992. !disableArrowKeyDown && focusableNum && this._handleTriggerArrowUpKeydown(focusableElements, event);
  993. break;
  994. case "ArrowDown":
  995. !disableArrowKeyDown && focusableNum && this._handleTriggerArrowDownKeydown(focusableElements, event);
  996. break;
  997. default:
  998. break;
  999. }
  1000. }
  1001. /**
  1002. * focus trigger
  1003. *
  1004. * when trigger is 'focus' or 'hover', onFocus is bind to show popup
  1005. * if we focus trigger, popup will show again
  1006. *
  1007. * 如果 trigger 是 focus 或者 hover,则它绑定了 onFocus,这里我们如果重新 focus 的话,popup 会再次打开
  1008. * 因此 returnFocusOnClose 只支持 click trigger
  1009. */
  1010. focusTrigger() {
  1011. const { trigger, returnFocusOnClose, preventScroll } = this.getProps();
  1012. if (returnFocusOnClose && trigger !== 'custom') {
  1013. const triggerNode = this._adapter.getTriggerNode();
  1014. if (triggerNode && 'focus' in triggerNode) {
  1015. triggerNode.focus({ preventScroll });
  1016. }
  1017. }
  1018. }
  1019. _handleEscKeyDown(event: any) {
  1020. const { trigger } = this.getProps();
  1021. if (trigger !== 'custom') {
  1022. // Move the focus into the trigger first and then close the pop-up layer
  1023. // to avoid the problem of opening the pop-up layer again when the focus returns to the trigger in the case of hover and focus
  1024. this.focusTrigger();
  1025. this.hide();
  1026. }
  1027. this._adapter.notifyEscKeydown(event);
  1028. }
  1029. _handleContainerTabKeyDown(focusableElements: any[], event: any) {
  1030. const { preventScroll } = this.getProps();
  1031. const activeElement = this._adapter.getActiveElement();
  1032. const isLastCurrentFocus = focusableElements[focusableElements.length - 1] === activeElement;
  1033. if (isLastCurrentFocus) {
  1034. focusableElements[0].focus({ preventScroll });
  1035. event.preventDefault(); // prevent browser default tab move behavior
  1036. }
  1037. }
  1038. _handleContainerShiftTabKeyDown(focusableElements: any[], event: any) {
  1039. const { preventScroll } = this.getProps();
  1040. const activeElement = this._adapter.getActiveElement();
  1041. const isFirstCurrentFocus = focusableElements[0] === activeElement;
  1042. if (isFirstCurrentFocus) {
  1043. focusableElements[focusableElements.length - 1].focus({ preventScroll });
  1044. event.preventDefault(); // prevent browser default tab move behavior
  1045. }
  1046. }
  1047. _handleTriggerArrowDownKeydown(focusableElements: any[], event: any) {
  1048. const { preventScroll } = this.getProps();
  1049. focusableElements[0].focus({ preventScroll });
  1050. event.preventDefault(); // prevent browser default scroll behavior
  1051. }
  1052. _handleTriggerArrowUpKeydown(focusableElements: any[], event: any) {
  1053. const { preventScroll } = this.getProps();
  1054. focusableElements[focusableElements.length - 1].focus({ preventScroll });
  1055. event.preventDefault(); // prevent browser default scroll behavior
  1056. }
  1057. }