foundation.ts 57 KB

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