foundation.ts 62 KB

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