tooltip.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. /*!
  2. * All material copyright ESRI, All Rights Reserved, unless otherwise specified.
  3. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details.
  4. * v1.0.0-beta.97
  5. */
  6. import { Host, h } from "@stencil/core";
  7. import { CSS, ARIA_DESCRIBED_BY } from "./resources";
  8. import { guid } from "../../utils/guid";
  9. import { connectFloatingUI, disconnectFloatingUI, defaultOffsetDistance, reposition, FloatingCSS, updateAfterClose } from "../../utils/floating-ui";
  10. import { queryElementRoots, toAriaBoolean } from "../../utils/dom";
  11. import TooltipManager from "./TooltipManager";
  12. const manager = new TooltipManager();
  13. /**
  14. * @slot - A slot for adding text.
  15. */
  16. export class Tooltip {
  17. constructor() {
  18. // --------------------------------------------------------------------------
  19. //
  20. // Properties
  21. //
  22. // --------------------------------------------------------------------------
  23. /** Closes the component when the `referenceElement` is clicked. */
  24. this.closeOnClick = false;
  25. /**
  26. * Offset the position of the component away from the `referenceElement`.
  27. *
  28. * @default 6
  29. */
  30. this.offsetDistance = defaultOffsetDistance;
  31. /**
  32. * Offset the position of the component along the `referenceElement`.
  33. */
  34. this.offsetSkidding = 0;
  35. /**
  36. * When `true`, the component is open.
  37. */
  38. this.open = false;
  39. /**
  40. * Determines the type of positioning to use for the overlaid content.
  41. *
  42. * Using `"absolute"` will work for most cases. The component will be positioned inside of overflowing parent containers and will affect the container's layout.
  43. *
  44. * The `"fixed"` value should be used to escape an overflowing parent container, or when the reference element's `position` CSS property is `"fixed"`.
  45. *
  46. */
  47. this.overlayPositioning = "absolute";
  48. /**
  49. * Determines where the component will be positioned relative to the `referenceElement`.
  50. *
  51. * @see [LogicalPlacement](https://github.com/Esri/calcite-components/blob/master/src/utils/floating-ui.ts#L25)
  52. */
  53. this.placement = "auto";
  54. this.guid = `calcite-tooltip-${guid()}`;
  55. this.hasLoaded = false;
  56. // --------------------------------------------------------------------------
  57. //
  58. // Private Methods
  59. //
  60. // --------------------------------------------------------------------------
  61. this.setUpReferenceElement = (warn = true) => {
  62. this.removeReferences();
  63. this.effectiveReferenceElement = this.getReferenceElement();
  64. connectFloatingUI(this, this.effectiveReferenceElement, this.el);
  65. const { el, referenceElement, effectiveReferenceElement } = this;
  66. if (warn && referenceElement && !effectiveReferenceElement) {
  67. console.warn(`${el.tagName}: reference-element id "${referenceElement}" was not found.`, {
  68. el
  69. });
  70. }
  71. this.addReferences();
  72. };
  73. this.getId = () => {
  74. return this.el.id || this.guid;
  75. };
  76. this.addReferences = () => {
  77. const { effectiveReferenceElement } = this;
  78. if (!effectiveReferenceElement) {
  79. return;
  80. }
  81. const id = this.getId();
  82. if ("setAttribute" in effectiveReferenceElement) {
  83. effectiveReferenceElement.setAttribute(ARIA_DESCRIBED_BY, id);
  84. }
  85. manager.registerElement(effectiveReferenceElement, this.el);
  86. };
  87. this.removeReferences = () => {
  88. const { effectiveReferenceElement } = this;
  89. if (!effectiveReferenceElement) {
  90. return;
  91. }
  92. if ("removeAttribute" in effectiveReferenceElement) {
  93. effectiveReferenceElement.removeAttribute(ARIA_DESCRIBED_BY);
  94. }
  95. manager.unregisterElement(effectiveReferenceElement);
  96. };
  97. }
  98. offsetDistanceOffsetHandler() {
  99. this.reposition(true);
  100. }
  101. offsetSkiddingHandler() {
  102. this.reposition(true);
  103. }
  104. openHandler(value) {
  105. if (value) {
  106. this.reposition(true);
  107. }
  108. else {
  109. updateAfterClose(this.el);
  110. }
  111. }
  112. overlayPositioningHandler() {
  113. this.reposition(true);
  114. }
  115. placementHandler() {
  116. this.reposition(true);
  117. }
  118. referenceElementHandler() {
  119. this.setUpReferenceElement();
  120. }
  121. // --------------------------------------------------------------------------
  122. //
  123. // Lifecycle
  124. //
  125. // --------------------------------------------------------------------------
  126. connectedCallback() {
  127. this.setUpReferenceElement(this.hasLoaded);
  128. }
  129. componentDidLoad() {
  130. if (this.referenceElement && !this.effectiveReferenceElement) {
  131. this.setUpReferenceElement();
  132. }
  133. this.reposition(true);
  134. this.hasLoaded = true;
  135. }
  136. disconnectedCallback() {
  137. this.removeReferences();
  138. disconnectFloatingUI(this, this.effectiveReferenceElement, this.el);
  139. }
  140. // --------------------------------------------------------------------------
  141. //
  142. // Public Methods
  143. //
  144. // --------------------------------------------------------------------------
  145. /**
  146. * Updates the position of the component.
  147. *
  148. * @param delayed
  149. */
  150. async reposition(delayed = false) {
  151. const { el, effectiveReferenceElement, placement, overlayPositioning, offsetDistance, offsetSkidding, arrowEl } = this;
  152. return reposition(this, {
  153. floatingEl: el,
  154. referenceEl: effectiveReferenceElement,
  155. overlayPositioning,
  156. placement,
  157. offsetDistance,
  158. offsetSkidding,
  159. includeArrow: true,
  160. arrowEl,
  161. type: "tooltip"
  162. }, delayed);
  163. }
  164. getReferenceElement() {
  165. const { referenceElement, el } = this;
  166. return ((typeof referenceElement === "string"
  167. ? queryElementRoots(el, { id: referenceElement })
  168. : referenceElement) || null);
  169. }
  170. // --------------------------------------------------------------------------
  171. //
  172. // Render Methods
  173. //
  174. // --------------------------------------------------------------------------
  175. render() {
  176. const { effectiveReferenceElement, label, open } = this;
  177. const displayed = effectiveReferenceElement && open;
  178. const hidden = !displayed;
  179. return (h(Host, { "aria-hidden": toAriaBoolean(hidden), "aria-label": label, "aria-live": "polite", "calcite-hydrated-hidden": hidden, id: this.getId(), role: "tooltip" }, h("div", { class: {
  180. [FloatingCSS.animation]: true,
  181. [FloatingCSS.animationActive]: displayed
  182. } }, h("div", { class: CSS.arrow, ref: (arrowEl) => (this.arrowEl = arrowEl) }), h("div", { class: CSS.container }, h("slot", null)))));
  183. }
  184. static get is() { return "calcite-tooltip"; }
  185. static get encapsulation() { return "shadow"; }
  186. static get originalStyleUrls() {
  187. return {
  188. "$": ["tooltip.scss"]
  189. };
  190. }
  191. static get styleUrls() {
  192. return {
  193. "$": ["tooltip.css"]
  194. };
  195. }
  196. static get properties() {
  197. return {
  198. "closeOnClick": {
  199. "type": "boolean",
  200. "mutable": false,
  201. "complexType": {
  202. "original": "boolean",
  203. "resolved": "boolean",
  204. "references": {}
  205. },
  206. "required": false,
  207. "optional": false,
  208. "docs": {
  209. "tags": [],
  210. "text": "Closes the component when the `referenceElement` is clicked."
  211. },
  212. "attribute": "close-on-click",
  213. "reflect": true,
  214. "defaultValue": "false"
  215. },
  216. "label": {
  217. "type": "string",
  218. "mutable": false,
  219. "complexType": {
  220. "original": "string",
  221. "resolved": "string",
  222. "references": {}
  223. },
  224. "required": true,
  225. "optional": false,
  226. "docs": {
  227. "tags": [],
  228. "text": "Accessible name for the component."
  229. },
  230. "attribute": "label",
  231. "reflect": false
  232. },
  233. "offsetDistance": {
  234. "type": "number",
  235. "mutable": false,
  236. "complexType": {
  237. "original": "number",
  238. "resolved": "number",
  239. "references": {}
  240. },
  241. "required": false,
  242. "optional": false,
  243. "docs": {
  244. "tags": [{
  245. "name": "default",
  246. "text": "6"
  247. }],
  248. "text": "Offset the position of the component away from the `referenceElement`."
  249. },
  250. "attribute": "offset-distance",
  251. "reflect": true,
  252. "defaultValue": "defaultOffsetDistance"
  253. },
  254. "offsetSkidding": {
  255. "type": "number",
  256. "mutable": false,
  257. "complexType": {
  258. "original": "number",
  259. "resolved": "number",
  260. "references": {}
  261. },
  262. "required": false,
  263. "optional": false,
  264. "docs": {
  265. "tags": [],
  266. "text": "Offset the position of the component along the `referenceElement`."
  267. },
  268. "attribute": "offset-skidding",
  269. "reflect": true,
  270. "defaultValue": "0"
  271. },
  272. "open": {
  273. "type": "boolean",
  274. "mutable": false,
  275. "complexType": {
  276. "original": "boolean",
  277. "resolved": "boolean",
  278. "references": {}
  279. },
  280. "required": false,
  281. "optional": false,
  282. "docs": {
  283. "tags": [],
  284. "text": "When `true`, the component is open."
  285. },
  286. "attribute": "open",
  287. "reflect": true,
  288. "defaultValue": "false"
  289. },
  290. "overlayPositioning": {
  291. "type": "string",
  292. "mutable": false,
  293. "complexType": {
  294. "original": "OverlayPositioning",
  295. "resolved": "\"absolute\" | \"fixed\"",
  296. "references": {
  297. "OverlayPositioning": {
  298. "location": "import",
  299. "path": "../../utils/floating-ui"
  300. }
  301. }
  302. },
  303. "required": false,
  304. "optional": false,
  305. "docs": {
  306. "tags": [],
  307. "text": "Determines the type of positioning to use for the overlaid content.\n\nUsing `\"absolute\"` will work for most cases. The component will be positioned inside of overflowing parent containers and will affect the container's layout.\n\nThe `\"fixed\"` value should be used to escape an overflowing parent container, or when the reference element's `position` CSS property is `\"fixed\"`."
  308. },
  309. "attribute": "overlay-positioning",
  310. "reflect": true,
  311. "defaultValue": "\"absolute\""
  312. },
  313. "placement": {
  314. "type": "string",
  315. "mutable": false,
  316. "complexType": {
  317. "original": "LogicalPlacement",
  318. "resolved": "Placement | VariationPlacement | AutoPlacement | DeprecatedPlacement",
  319. "references": {
  320. "LogicalPlacement": {
  321. "location": "import",
  322. "path": "../../utils/floating-ui"
  323. }
  324. }
  325. },
  326. "required": false,
  327. "optional": false,
  328. "docs": {
  329. "tags": [{
  330. "name": "see",
  331. "text": "[LogicalPlacement](https://github.com/Esri/calcite-components/blob/master/src/utils/floating-ui.ts#L25)"
  332. }],
  333. "text": "Determines where the component will be positioned relative to the `referenceElement`."
  334. },
  335. "attribute": "placement",
  336. "reflect": true,
  337. "defaultValue": "\"auto\""
  338. },
  339. "referenceElement": {
  340. "type": "string",
  341. "mutable": false,
  342. "complexType": {
  343. "original": "ReferenceElement | string",
  344. "resolved": "Element | VirtualElement | string",
  345. "references": {
  346. "ReferenceElement": {
  347. "location": "import",
  348. "path": "../../utils/floating-ui"
  349. }
  350. }
  351. },
  352. "required": false,
  353. "optional": false,
  354. "docs": {
  355. "tags": [],
  356. "text": "The `referenceElement` to position the component according to its `\"placement\"` value.\n\nSetting to the `HTMLElement` is preferred so the component does not need to query the DOM for the `referenceElement`.\n\nHowever, a string ID of the reference element can be used."
  357. },
  358. "attribute": "reference-element",
  359. "reflect": false
  360. }
  361. };
  362. }
  363. static get states() {
  364. return {
  365. "effectiveReferenceElement": {}
  366. };
  367. }
  368. static get methods() {
  369. return {
  370. "reposition": {
  371. "complexType": {
  372. "signature": "(delayed?: boolean) => Promise<void>",
  373. "parameters": [{
  374. "tags": [{
  375. "name": "param",
  376. "text": "delayed"
  377. }],
  378. "text": ""
  379. }],
  380. "references": {
  381. "Promise": {
  382. "location": "global"
  383. }
  384. },
  385. "return": "Promise<void>"
  386. },
  387. "docs": {
  388. "text": "Updates the position of the component.",
  389. "tags": [{
  390. "name": "param",
  391. "text": "delayed"
  392. }]
  393. }
  394. }
  395. };
  396. }
  397. static get elementRef() { return "el"; }
  398. static get watchers() {
  399. return [{
  400. "propName": "offsetDistance",
  401. "methodName": "offsetDistanceOffsetHandler"
  402. }, {
  403. "propName": "offsetSkidding",
  404. "methodName": "offsetSkiddingHandler"
  405. }, {
  406. "propName": "open",
  407. "methodName": "openHandler"
  408. }, {
  409. "propName": "overlayPositioning",
  410. "methodName": "overlayPositioningHandler"
  411. }, {
  412. "propName": "placement",
  413. "methodName": "placementHandler"
  414. }, {
  415. "propName": "referenceElement",
  416. "methodName": "referenceElementHandler"
  417. }];
  418. }
  419. }