tooltip.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  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.82
  5. */
  6. import { Component, Element, Host, Method, Prop, State, Watch, h } from "@stencil/core";
  7. import { CSS, ARIA_DESCRIBED_BY } from "./resources";
  8. import { guid } from "../../utils/guid";
  9. import { defaultOffsetDistance, createPopper, updatePopper, CSS as PopperCSS } from "../../utils/popper";
  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. * Offset the position of the tooltip away from the reference element.
  20. * @default 6
  21. */
  22. this.offsetDistance = defaultOffsetDistance;
  23. /**
  24. * Offset the position of the tooltip along the reference element.
  25. */
  26. this.offsetSkidding = 0;
  27. /**
  28. * Display and position the component.
  29. */
  30. this.open = false;
  31. /** Describes the type of positioning to use for the overlaid content. If your element is in a fixed container, use the 'fixed' value. */
  32. this.overlayPositioning = "absolute";
  33. /**
  34. * Determines where the component will be positioned relative to the referenceElement.
  35. * @see [PopperPlacement](https://github.com/Esri/calcite-components/blob/master/src/utils/popper.ts#L25)
  36. */
  37. this.placement = "auto";
  38. this.guid = `calcite-tooltip-${guid()}`;
  39. // --------------------------------------------------------------------------
  40. //
  41. // Private Methods
  42. //
  43. // --------------------------------------------------------------------------
  44. this.setUpReferenceElement = () => {
  45. this.removeReferences();
  46. this.effectiveReferenceElement = this.getReferenceElement();
  47. const { el, referenceElement, effectiveReferenceElement } = this;
  48. if (referenceElement && !effectiveReferenceElement) {
  49. console.warn(`${el.tagName}: reference-element id "${referenceElement}" was not found.`, {
  50. el
  51. });
  52. }
  53. this.addReferences();
  54. this.createPopper();
  55. };
  56. this.getId = () => {
  57. return this.el.id || this.guid;
  58. };
  59. this.addReferences = () => {
  60. const { effectiveReferenceElement } = this;
  61. if (!effectiveReferenceElement) {
  62. return;
  63. }
  64. const id = this.getId();
  65. effectiveReferenceElement.setAttribute(ARIA_DESCRIBED_BY, id);
  66. manager.registerElement(effectiveReferenceElement, this.el);
  67. };
  68. this.removeReferences = () => {
  69. const { effectiveReferenceElement } = this;
  70. if (!effectiveReferenceElement) {
  71. return;
  72. }
  73. effectiveReferenceElement.removeAttribute(ARIA_DESCRIBED_BY);
  74. manager.unregisterElement(effectiveReferenceElement);
  75. };
  76. }
  77. offsetDistanceOffsetHandler() {
  78. this.reposition();
  79. }
  80. offsetSkiddingHandler() {
  81. this.reposition();
  82. }
  83. openHandler() {
  84. this.reposition();
  85. }
  86. placementHandler() {
  87. this.reposition();
  88. }
  89. referenceElementHandler() {
  90. this.setUpReferenceElement();
  91. }
  92. // --------------------------------------------------------------------------
  93. //
  94. // Lifecycle
  95. //
  96. // --------------------------------------------------------------------------
  97. componentWillLoad() {
  98. this.setUpReferenceElement();
  99. }
  100. componentDidLoad() {
  101. this.reposition();
  102. }
  103. disconnectedCallback() {
  104. this.removeReferences();
  105. this.destroyPopper();
  106. }
  107. // --------------------------------------------------------------------------
  108. //
  109. // Public Methods
  110. //
  111. // --------------------------------------------------------------------------
  112. /** Updates the position of the component. */
  113. async reposition() {
  114. const { popper, el, placement } = this;
  115. const modifiers = this.getModifiers();
  116. popper
  117. ? await updatePopper({
  118. el,
  119. modifiers,
  120. placement,
  121. popper
  122. })
  123. : this.createPopper();
  124. }
  125. getReferenceElement() {
  126. const { referenceElement, el } = this;
  127. return ((typeof referenceElement === "string"
  128. ? queryElementRoots(el, { id: referenceElement })
  129. : referenceElement) || null);
  130. }
  131. getModifiers() {
  132. const { arrowEl, offsetDistance, offsetSkidding } = this;
  133. const arrowModifier = {
  134. name: "arrow",
  135. enabled: true,
  136. options: {
  137. element: arrowEl
  138. }
  139. };
  140. const offsetModifier = {
  141. name: "offset",
  142. enabled: true,
  143. options: {
  144. offset: [offsetSkidding, offsetDistance]
  145. }
  146. };
  147. const eventListenerModifier = {
  148. name: "eventListeners",
  149. enabled: this.open
  150. };
  151. return [arrowModifier, offsetModifier, eventListenerModifier];
  152. }
  153. createPopper() {
  154. this.destroyPopper();
  155. const { el, placement, effectiveReferenceElement: referenceEl, overlayPositioning } = this;
  156. const modifiers = this.getModifiers();
  157. this.popper = createPopper({
  158. el,
  159. modifiers,
  160. placement,
  161. overlayPositioning,
  162. referenceEl
  163. });
  164. }
  165. destroyPopper() {
  166. const { popper } = this;
  167. if (popper) {
  168. popper.destroy();
  169. }
  170. this.popper = null;
  171. }
  172. // --------------------------------------------------------------------------
  173. //
  174. // Render Methods
  175. //
  176. // --------------------------------------------------------------------------
  177. render() {
  178. const { effectiveReferenceElement, label, open } = this;
  179. const displayed = effectiveReferenceElement && open;
  180. const hidden = !displayed;
  181. return (h(Host, { "aria-hidden": toAriaBoolean(hidden), "aria-label": label, "calcite-hydrated-hidden": hidden, id: this.getId(), role: "tooltip" },
  182. h("div", { class: {
  183. [PopperCSS.animation]: true,
  184. [PopperCSS.animationActive]: displayed
  185. } },
  186. h("div", { class: CSS.arrow, ref: (arrowEl) => (this.arrowEl = arrowEl) }),
  187. h("div", { class: CSS.container },
  188. h("slot", null)))));
  189. }
  190. static get is() { return "calcite-tooltip"; }
  191. static get encapsulation() { return "shadow"; }
  192. static get originalStyleUrls() { return {
  193. "$": ["tooltip.scss"]
  194. }; }
  195. static get styleUrls() { return {
  196. "$": ["tooltip.css"]
  197. }; }
  198. static get properties() { return {
  199. "label": {
  200. "type": "string",
  201. "mutable": false,
  202. "complexType": {
  203. "original": "string",
  204. "resolved": "string",
  205. "references": {}
  206. },
  207. "required": true,
  208. "optional": false,
  209. "docs": {
  210. "tags": [],
  211. "text": "Accessible name for the component"
  212. },
  213. "attribute": "label",
  214. "reflect": false
  215. },
  216. "offsetDistance": {
  217. "type": "number",
  218. "mutable": false,
  219. "complexType": {
  220. "original": "number",
  221. "resolved": "number",
  222. "references": {}
  223. },
  224. "required": false,
  225. "optional": false,
  226. "docs": {
  227. "tags": [{
  228. "name": "default",
  229. "text": "6"
  230. }],
  231. "text": "Offset the position of the tooltip away from the reference element."
  232. },
  233. "attribute": "offset-distance",
  234. "reflect": true,
  235. "defaultValue": "defaultOffsetDistance"
  236. },
  237. "offsetSkidding": {
  238. "type": "number",
  239. "mutable": false,
  240. "complexType": {
  241. "original": "number",
  242. "resolved": "number",
  243. "references": {}
  244. },
  245. "required": false,
  246. "optional": false,
  247. "docs": {
  248. "tags": [],
  249. "text": "Offset the position of the tooltip along the reference element."
  250. },
  251. "attribute": "offset-skidding",
  252. "reflect": true,
  253. "defaultValue": "0"
  254. },
  255. "open": {
  256. "type": "boolean",
  257. "mutable": false,
  258. "complexType": {
  259. "original": "boolean",
  260. "resolved": "boolean",
  261. "references": {}
  262. },
  263. "required": false,
  264. "optional": false,
  265. "docs": {
  266. "tags": [],
  267. "text": "Display and position the component."
  268. },
  269. "attribute": "open",
  270. "reflect": true,
  271. "defaultValue": "false"
  272. },
  273. "overlayPositioning": {
  274. "type": "string",
  275. "mutable": false,
  276. "complexType": {
  277. "original": "OverlayPositioning",
  278. "resolved": "\"absolute\" | \"fixed\"",
  279. "references": {
  280. "OverlayPositioning": {
  281. "location": "import",
  282. "path": "../../utils/popper"
  283. }
  284. }
  285. },
  286. "required": false,
  287. "optional": false,
  288. "docs": {
  289. "tags": [],
  290. "text": "Describes the type of positioning to use for the overlaid content. If your element is in a fixed container, use the 'fixed' value."
  291. },
  292. "attribute": "overlay-positioning",
  293. "reflect": false,
  294. "defaultValue": "\"absolute\""
  295. },
  296. "placement": {
  297. "type": "string",
  298. "mutable": false,
  299. "complexType": {
  300. "original": "PopperPlacement",
  301. "resolved": "Placement | PlacementRtl | VariationRtl",
  302. "references": {
  303. "PopperPlacement": {
  304. "location": "import",
  305. "path": "../../utils/popper"
  306. }
  307. }
  308. },
  309. "required": false,
  310. "optional": false,
  311. "docs": {
  312. "tags": [{
  313. "name": "see",
  314. "text": "[PopperPlacement](https://github.com/Esri/calcite-components/blob/master/src/utils/popper.ts#L25)"
  315. }],
  316. "text": "Determines where the component will be positioned relative to the referenceElement."
  317. },
  318. "attribute": "placement",
  319. "reflect": true,
  320. "defaultValue": "\"auto\""
  321. },
  322. "referenceElement": {
  323. "type": "string",
  324. "mutable": false,
  325. "complexType": {
  326. "original": "HTMLElement | string",
  327. "resolved": "HTMLElement | string",
  328. "references": {
  329. "HTMLElement": {
  330. "location": "global"
  331. }
  332. }
  333. },
  334. "required": false,
  335. "optional": false,
  336. "docs": {
  337. "tags": [],
  338. "text": "Reference HTMLElement used to position this component according to the placement property. As a convenience, a string ID of the reference element can be used. However, setting this property to use an HTMLElement is preferred so that the component does not need to query the DOM for the referenceElement."
  339. },
  340. "attribute": "reference-element",
  341. "reflect": false
  342. }
  343. }; }
  344. static get states() { return {
  345. "effectiveReferenceElement": {}
  346. }; }
  347. static get methods() { return {
  348. "reposition": {
  349. "complexType": {
  350. "signature": "() => Promise<void>",
  351. "parameters": [],
  352. "references": {
  353. "Promise": {
  354. "location": "global"
  355. }
  356. },
  357. "return": "Promise<void>"
  358. },
  359. "docs": {
  360. "text": "Updates the position of the component.",
  361. "tags": []
  362. }
  363. }
  364. }; }
  365. static get elementRef() { return "el"; }
  366. static get watchers() { return [{
  367. "propName": "offsetDistance",
  368. "methodName": "offsetDistanceOffsetHandler"
  369. }, {
  370. "propName": "offsetSkidding",
  371. "methodName": "offsetSkiddingHandler"
  372. }, {
  373. "propName": "open",
  374. "methodName": "openHandler"
  375. }, {
  376. "propName": "placement",
  377. "methodName": "placementHandler"
  378. }, {
  379. "propName": "referenceElement",
  380. "methodName": "referenceElementHandler"
  381. }]; }
  382. }