foundation.ts 60 KB

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