foundation.ts 61 KB

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