foundation.ts 57 KB

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