foundation.ts 60 KB

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