foundation.ts 56 KB

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