popover.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  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, Event, forceUpdate, Host, Method, Prop, State, Watch, h } from "@stencil/core";
  7. import { CSS, ARIA_CONTROLS, ARIA_EXPANDED, HEADING_LEVEL, TEXT, defaultPopoverPlacement } from "./resources";
  8. import { defaultOffsetDistance, createPopper, updatePopper, CSS as PopperCSS, filterComputedPlacements } from "../../utils/popper";
  9. import { guid } from "../../utils/guid";
  10. import { queryElementRoots, toAriaBoolean } from "../../utils/dom";
  11. import { Heading } from "../functional/Heading";
  12. import PopoverManager from "./PopoverManager";
  13. const manager = new PopoverManager();
  14. /**
  15. * @slot - A slot for adding custom content.
  16. */
  17. export class Popover {
  18. constructor() {
  19. // --------------------------------------------------------------------------
  20. //
  21. // Properties
  22. //
  23. // --------------------------------------------------------------------------
  24. /**
  25. * Automatically closes any currently open popovers when clicking outside of a popover.
  26. */
  27. this.autoClose = false;
  28. /**
  29. * Display a close button within the Popover.
  30. * @deprecated use dismissible instead.
  31. */
  32. this.closeButton = false;
  33. /**
  34. * Display a close button within the Popover.
  35. */
  36. this.dismissible = false;
  37. /**
  38. * Prevents flipping the popover's placement when it starts to overlap its reference element.
  39. */
  40. this.disableFlip = false;
  41. /**
  42. * Removes the caret pointer.
  43. */
  44. this.disablePointer = false;
  45. /**
  46. * Offset the position of the popover away from the reference element.
  47. * @default 6
  48. */
  49. this.offsetDistance = defaultOffsetDistance;
  50. /**
  51. * Offset the position of the popover along the reference element.
  52. */
  53. this.offsetSkidding = 0;
  54. /**
  55. * Display and position the component.
  56. */
  57. this.open = false;
  58. /** Describes the type of positioning to use for the overlaid content. If your element is in a fixed container, use the 'fixed' value. */
  59. this.overlayPositioning = "absolute";
  60. /**
  61. * Determines where the component will be positioned relative to the referenceElement.
  62. * @see [PopperPlacement](https://github.com/Esri/calcite-components/blob/master/src/utils/popper.ts#L25)
  63. */
  64. this.placement = defaultPopoverPlacement;
  65. /** Text for close button.
  66. * @default "Close"
  67. */
  68. this.intlClose = TEXT.close;
  69. this.guid = `calcite-popover-${guid()}`;
  70. this.activeTransitionProp = "opacity";
  71. // --------------------------------------------------------------------------
  72. //
  73. // Private Methods
  74. //
  75. // --------------------------------------------------------------------------
  76. this.setFilteredPlacements = () => {
  77. const { el, flipPlacements } = this;
  78. this.filteredFlipPlacements = flipPlacements
  79. ? filterComputedPlacements(flipPlacements, el)
  80. : null;
  81. };
  82. this.setUpReferenceElement = () => {
  83. this.removeReferences();
  84. this.effectiveReferenceElement = this.getReferenceElement();
  85. const { el, referenceElement, effectiveReferenceElement } = this;
  86. if (referenceElement && !effectiveReferenceElement) {
  87. console.warn(`${el.tagName}: reference-element id "${referenceElement}" was not found.`, {
  88. el
  89. });
  90. }
  91. this.addReferences();
  92. this.createPopper();
  93. };
  94. this.getId = () => {
  95. return this.el.id || this.guid;
  96. };
  97. this.setExpandedAttr = () => {
  98. const { effectiveReferenceElement, open } = this;
  99. if (!effectiveReferenceElement) {
  100. return;
  101. }
  102. effectiveReferenceElement.setAttribute(ARIA_EXPANDED, toAriaBoolean(open));
  103. };
  104. this.addReferences = () => {
  105. const { effectiveReferenceElement } = this;
  106. if (!effectiveReferenceElement) {
  107. return;
  108. }
  109. const id = this.getId();
  110. effectiveReferenceElement.setAttribute(ARIA_CONTROLS, id);
  111. manager.registerElement(effectiveReferenceElement, this.el);
  112. this.setExpandedAttr();
  113. };
  114. this.removeReferences = () => {
  115. const { effectiveReferenceElement } = this;
  116. if (!effectiveReferenceElement) {
  117. return;
  118. }
  119. effectiveReferenceElement.removeAttribute(ARIA_CONTROLS);
  120. effectiveReferenceElement.removeAttribute(ARIA_EXPANDED);
  121. manager.unregisterElement(effectiveReferenceElement);
  122. };
  123. this.hide = () => {
  124. this.open = false;
  125. };
  126. this.transitionEnd = (event) => {
  127. if (event.propertyName === this.activeTransitionProp) {
  128. this.open ? this.calcitePopoverOpen.emit() : this.calcitePopoverClose.emit();
  129. }
  130. };
  131. }
  132. flipPlacementsHandler() {
  133. this.setFilteredPlacements();
  134. }
  135. offsetDistanceOffsetHandler() {
  136. this.reposition();
  137. }
  138. offsetSkiddingHandler() {
  139. this.reposition();
  140. }
  141. openHandler() {
  142. this.reposition();
  143. this.setExpandedAttr();
  144. }
  145. placementHandler() {
  146. this.reposition();
  147. }
  148. referenceElementHandler() {
  149. this.setUpReferenceElement();
  150. }
  151. // --------------------------------------------------------------------------
  152. //
  153. // Lifecycle
  154. //
  155. // --------------------------------------------------------------------------
  156. connectedCallback() {
  157. this.setFilteredPlacements();
  158. }
  159. componentWillLoad() {
  160. this.setUpReferenceElement();
  161. }
  162. componentDidLoad() {
  163. this.reposition();
  164. }
  165. disconnectedCallback() {
  166. this.removeReferences();
  167. this.destroyPopper();
  168. }
  169. // --------------------------------------------------------------------------
  170. //
  171. // Public Methods
  172. //
  173. // --------------------------------------------------------------------------
  174. /** Updates the position of the component. */
  175. async reposition() {
  176. const { popper, el, placement } = this;
  177. const modifiers = this.getModifiers();
  178. popper
  179. ? await updatePopper({
  180. el,
  181. modifiers,
  182. placement,
  183. popper
  184. })
  185. : this.createPopper();
  186. }
  187. /** Sets focus on the component. */
  188. async setFocus(focusId) {
  189. var _a;
  190. const { closeButtonEl } = this;
  191. if (focusId === "close-button" && closeButtonEl) {
  192. forceUpdate(closeButtonEl);
  193. closeButtonEl.setFocus();
  194. return;
  195. }
  196. (_a = this.el) === null || _a === void 0 ? void 0 : _a.focus();
  197. }
  198. /** Toggles the popover's open property. */
  199. async toggle(value = !this.open) {
  200. this.open = value;
  201. }
  202. getReferenceElement() {
  203. const { referenceElement, el } = this;
  204. return ((typeof referenceElement === "string"
  205. ? queryElementRoots(el, { id: referenceElement })
  206. : referenceElement) || null);
  207. }
  208. getModifiers() {
  209. const { arrowEl, disableFlip, disablePointer, offsetDistance, offsetSkidding, filteredFlipPlacements } = this;
  210. const flipModifier = {
  211. name: "flip",
  212. enabled: !disableFlip
  213. };
  214. if (filteredFlipPlacements) {
  215. flipModifier.options = {
  216. fallbackPlacements: filteredFlipPlacements
  217. };
  218. }
  219. const arrowModifier = {
  220. name: "arrow",
  221. enabled: !disablePointer
  222. };
  223. if (arrowEl) {
  224. arrowModifier.options = {
  225. element: arrowEl
  226. };
  227. }
  228. const offsetModifier = {
  229. name: "offset",
  230. enabled: true,
  231. options: {
  232. offset: [offsetSkidding, offsetDistance]
  233. }
  234. };
  235. const eventListenerModifier = {
  236. name: "eventListeners",
  237. enabled: this.open
  238. };
  239. return [arrowModifier, flipModifier, offsetModifier, eventListenerModifier];
  240. }
  241. createPopper() {
  242. this.destroyPopper();
  243. const { el, placement, effectiveReferenceElement: referenceEl, overlayPositioning } = this;
  244. const modifiers = this.getModifiers();
  245. this.popper = createPopper({
  246. el,
  247. modifiers,
  248. overlayPositioning,
  249. placement,
  250. referenceEl
  251. });
  252. }
  253. destroyPopper() {
  254. const { popper } = this;
  255. if (popper) {
  256. popper.destroy();
  257. }
  258. this.popper = null;
  259. }
  260. // --------------------------------------------------------------------------
  261. //
  262. // Render Methods
  263. //
  264. // --------------------------------------------------------------------------
  265. renderCloseButton() {
  266. const { dismissible, closeButton, intlClose } = this;
  267. return dismissible || closeButton ? (h("div", { class: CSS.closeButtonContainer },
  268. h("calcite-action", { class: CSS.closeButton, onClick: this.hide, ref: (closeButtonEl) => (this.closeButtonEl = closeButtonEl), text: intlClose },
  269. h("calcite-icon", { icon: "x", scale: "m" })))) : null;
  270. }
  271. renderHeader() {
  272. const { heading, headingLevel } = this;
  273. const headingNode = heading ? (h(Heading, { class: CSS.heading, level: headingLevel || HEADING_LEVEL }, heading)) : null;
  274. return headingNode ? (h("div", { class: CSS.header },
  275. headingNode,
  276. this.renderCloseButton())) : null;
  277. }
  278. render() {
  279. const { effectiveReferenceElement, heading, label, open, disablePointer } = this;
  280. const displayed = effectiveReferenceElement && open;
  281. const hidden = !displayed;
  282. const arrowNode = !disablePointer ? (h("div", { class: CSS.arrow, ref: (arrowEl) => (this.arrowEl = arrowEl) })) : null;
  283. return (h(Host, { "aria-hidden": toAriaBoolean(hidden), "aria-label": label, "calcite-hydrated-hidden": hidden, id: this.getId(), role: "dialog" },
  284. h("div", { class: {
  285. [PopperCSS.animation]: true,
  286. [PopperCSS.animationActive]: displayed
  287. }, onTransitionEnd: this.transitionEnd },
  288. arrowNode,
  289. h("div", { class: {
  290. [CSS.hasHeader]: !!heading,
  291. [CSS.container]: true
  292. } },
  293. this.renderHeader(),
  294. h("div", { class: CSS.content },
  295. h("slot", null)),
  296. !heading ? this.renderCloseButton() : null))));
  297. }
  298. static get is() { return "calcite-popover"; }
  299. static get encapsulation() { return "shadow"; }
  300. static get originalStyleUrls() { return {
  301. "$": ["popover.scss"]
  302. }; }
  303. static get styleUrls() { return {
  304. "$": ["popover.css"]
  305. }; }
  306. static get properties() { return {
  307. "autoClose": {
  308. "type": "boolean",
  309. "mutable": false,
  310. "complexType": {
  311. "original": "boolean",
  312. "resolved": "boolean",
  313. "references": {}
  314. },
  315. "required": false,
  316. "optional": false,
  317. "docs": {
  318. "tags": [],
  319. "text": "Automatically closes any currently open popovers when clicking outside of a popover."
  320. },
  321. "attribute": "auto-close",
  322. "reflect": true,
  323. "defaultValue": "false"
  324. },
  325. "closeButton": {
  326. "type": "boolean",
  327. "mutable": false,
  328. "complexType": {
  329. "original": "boolean",
  330. "resolved": "boolean",
  331. "references": {}
  332. },
  333. "required": false,
  334. "optional": false,
  335. "docs": {
  336. "tags": [{
  337. "name": "deprecated",
  338. "text": "use dismissible instead."
  339. }],
  340. "text": "Display a close button within the Popover."
  341. },
  342. "attribute": "close-button",
  343. "reflect": true,
  344. "defaultValue": "false"
  345. },
  346. "dismissible": {
  347. "type": "boolean",
  348. "mutable": false,
  349. "complexType": {
  350. "original": "boolean",
  351. "resolved": "boolean",
  352. "references": {}
  353. },
  354. "required": false,
  355. "optional": false,
  356. "docs": {
  357. "tags": [],
  358. "text": "Display a close button within the Popover."
  359. },
  360. "attribute": "dismissible",
  361. "reflect": true,
  362. "defaultValue": "false"
  363. },
  364. "disableFlip": {
  365. "type": "boolean",
  366. "mutable": false,
  367. "complexType": {
  368. "original": "boolean",
  369. "resolved": "boolean",
  370. "references": {}
  371. },
  372. "required": false,
  373. "optional": false,
  374. "docs": {
  375. "tags": [],
  376. "text": "Prevents flipping the popover's placement when it starts to overlap its reference element."
  377. },
  378. "attribute": "disable-flip",
  379. "reflect": true,
  380. "defaultValue": "false"
  381. },
  382. "disablePointer": {
  383. "type": "boolean",
  384. "mutable": false,
  385. "complexType": {
  386. "original": "boolean",
  387. "resolved": "boolean",
  388. "references": {}
  389. },
  390. "required": false,
  391. "optional": false,
  392. "docs": {
  393. "tags": [],
  394. "text": "Removes the caret pointer."
  395. },
  396. "attribute": "disable-pointer",
  397. "reflect": true,
  398. "defaultValue": "false"
  399. },
  400. "flipPlacements": {
  401. "type": "unknown",
  402. "mutable": false,
  403. "complexType": {
  404. "original": "ComputedPlacement[]",
  405. "resolved": "ComputedPlacement[]",
  406. "references": {
  407. "ComputedPlacement": {
  408. "location": "import",
  409. "path": "../../utils/popper"
  410. }
  411. }
  412. },
  413. "required": false,
  414. "optional": true,
  415. "docs": {
  416. "tags": [],
  417. "text": "Defines the available placements that can be used when a flip occurs."
  418. }
  419. },
  420. "heading": {
  421. "type": "string",
  422. "mutable": false,
  423. "complexType": {
  424. "original": "string",
  425. "resolved": "string",
  426. "references": {}
  427. },
  428. "required": false,
  429. "optional": true,
  430. "docs": {
  431. "tags": [],
  432. "text": "Heading text."
  433. },
  434. "attribute": "heading",
  435. "reflect": false
  436. },
  437. "headingLevel": {
  438. "type": "number",
  439. "mutable": false,
  440. "complexType": {
  441. "original": "HeadingLevel",
  442. "resolved": "1 | 2 | 3 | 4 | 5 | 6",
  443. "references": {
  444. "HeadingLevel": {
  445. "location": "import",
  446. "path": "../functional/Heading"
  447. }
  448. }
  449. },
  450. "required": false,
  451. "optional": false,
  452. "docs": {
  453. "tags": [],
  454. "text": "Number at which section headings should start for this component."
  455. },
  456. "attribute": "heading-level",
  457. "reflect": false
  458. },
  459. "label": {
  460. "type": "string",
  461. "mutable": false,
  462. "complexType": {
  463. "original": "string",
  464. "resolved": "string",
  465. "references": {}
  466. },
  467. "required": true,
  468. "optional": false,
  469. "docs": {
  470. "tags": [],
  471. "text": "Accessible name for the component"
  472. },
  473. "attribute": "label",
  474. "reflect": false
  475. },
  476. "offsetDistance": {
  477. "type": "number",
  478. "mutable": false,
  479. "complexType": {
  480. "original": "number",
  481. "resolved": "number",
  482. "references": {}
  483. },
  484. "required": false,
  485. "optional": false,
  486. "docs": {
  487. "tags": [{
  488. "name": "default",
  489. "text": "6"
  490. }],
  491. "text": "Offset the position of the popover away from the reference element."
  492. },
  493. "attribute": "offset-distance",
  494. "reflect": true,
  495. "defaultValue": "defaultOffsetDistance"
  496. },
  497. "offsetSkidding": {
  498. "type": "number",
  499. "mutable": false,
  500. "complexType": {
  501. "original": "number",
  502. "resolved": "number",
  503. "references": {}
  504. },
  505. "required": false,
  506. "optional": false,
  507. "docs": {
  508. "tags": [],
  509. "text": "Offset the position of the popover along the reference element."
  510. },
  511. "attribute": "offset-skidding",
  512. "reflect": true,
  513. "defaultValue": "0"
  514. },
  515. "open": {
  516. "type": "boolean",
  517. "mutable": true,
  518. "complexType": {
  519. "original": "boolean",
  520. "resolved": "boolean",
  521. "references": {}
  522. },
  523. "required": false,
  524. "optional": false,
  525. "docs": {
  526. "tags": [],
  527. "text": "Display and position the component."
  528. },
  529. "attribute": "open",
  530. "reflect": true,
  531. "defaultValue": "false"
  532. },
  533. "overlayPositioning": {
  534. "type": "string",
  535. "mutable": false,
  536. "complexType": {
  537. "original": "OverlayPositioning",
  538. "resolved": "\"absolute\" | \"fixed\"",
  539. "references": {
  540. "OverlayPositioning": {
  541. "location": "import",
  542. "path": "../../utils/popper"
  543. }
  544. }
  545. },
  546. "required": false,
  547. "optional": false,
  548. "docs": {
  549. "tags": [],
  550. "text": "Describes the type of positioning to use for the overlaid content. If your element is in a fixed container, use the 'fixed' value."
  551. },
  552. "attribute": "overlay-positioning",
  553. "reflect": false,
  554. "defaultValue": "\"absolute\""
  555. },
  556. "placement": {
  557. "type": "string",
  558. "mutable": false,
  559. "complexType": {
  560. "original": "PopperPlacement",
  561. "resolved": "Placement | PlacementRtl | VariationRtl",
  562. "references": {
  563. "PopperPlacement": {
  564. "location": "import",
  565. "path": "../../utils/popper"
  566. }
  567. }
  568. },
  569. "required": false,
  570. "optional": false,
  571. "docs": {
  572. "tags": [{
  573. "name": "see",
  574. "text": "[PopperPlacement](https://github.com/Esri/calcite-components/blob/master/src/utils/popper.ts#L25)"
  575. }],
  576. "text": "Determines where the component will be positioned relative to the referenceElement."
  577. },
  578. "attribute": "placement",
  579. "reflect": true,
  580. "defaultValue": "defaultPopoverPlacement"
  581. },
  582. "referenceElement": {
  583. "type": "string",
  584. "mutable": false,
  585. "complexType": {
  586. "original": "HTMLElement | string",
  587. "resolved": "HTMLElement | string",
  588. "references": {
  589. "HTMLElement": {
  590. "location": "global"
  591. }
  592. }
  593. },
  594. "required": true,
  595. "optional": false,
  596. "docs": {
  597. "tags": [],
  598. "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."
  599. },
  600. "attribute": "reference-element",
  601. "reflect": false
  602. },
  603. "intlClose": {
  604. "type": "string",
  605. "mutable": false,
  606. "complexType": {
  607. "original": "string",
  608. "resolved": "string",
  609. "references": {}
  610. },
  611. "required": false,
  612. "optional": false,
  613. "docs": {
  614. "tags": [{
  615. "name": "default",
  616. "text": "\"Close\""
  617. }],
  618. "text": "Text for close button."
  619. },
  620. "attribute": "intl-close",
  621. "reflect": false,
  622. "defaultValue": "TEXT.close"
  623. }
  624. }; }
  625. static get states() { return {
  626. "effectiveReferenceElement": {}
  627. }; }
  628. static get events() { return [{
  629. "method": "calcitePopoverClose",
  630. "name": "calcitePopoverClose",
  631. "bubbles": true,
  632. "cancelable": true,
  633. "composed": true,
  634. "docs": {
  635. "tags": [],
  636. "text": "Fired when the popover is closed"
  637. },
  638. "complexType": {
  639. "original": "any",
  640. "resolved": "any",
  641. "references": {}
  642. }
  643. }, {
  644. "method": "calcitePopoverOpen",
  645. "name": "calcitePopoverOpen",
  646. "bubbles": true,
  647. "cancelable": true,
  648. "composed": true,
  649. "docs": {
  650. "tags": [],
  651. "text": "Fired when the popover is opened"
  652. },
  653. "complexType": {
  654. "original": "any",
  655. "resolved": "any",
  656. "references": {}
  657. }
  658. }]; }
  659. static get methods() { return {
  660. "reposition": {
  661. "complexType": {
  662. "signature": "() => Promise<void>",
  663. "parameters": [],
  664. "references": {
  665. "Promise": {
  666. "location": "global"
  667. }
  668. },
  669. "return": "Promise<void>"
  670. },
  671. "docs": {
  672. "text": "Updates the position of the component.",
  673. "tags": []
  674. }
  675. },
  676. "setFocus": {
  677. "complexType": {
  678. "signature": "(focusId?: \"close-button\") => Promise<void>",
  679. "parameters": [{
  680. "tags": [],
  681. "text": ""
  682. }],
  683. "references": {
  684. "Promise": {
  685. "location": "global"
  686. }
  687. },
  688. "return": "Promise<void>"
  689. },
  690. "docs": {
  691. "text": "Sets focus on the component.",
  692. "tags": []
  693. }
  694. },
  695. "toggle": {
  696. "complexType": {
  697. "signature": "(value?: boolean) => Promise<void>",
  698. "parameters": [{
  699. "tags": [],
  700. "text": ""
  701. }],
  702. "references": {
  703. "Promise": {
  704. "location": "global"
  705. }
  706. },
  707. "return": "Promise<void>"
  708. },
  709. "docs": {
  710. "text": "Toggles the popover's open property.",
  711. "tags": []
  712. }
  713. }
  714. }; }
  715. static get elementRef() { return "el"; }
  716. static get watchers() { return [{
  717. "propName": "flipPlacements",
  718. "methodName": "flipPlacementsHandler"
  719. }, {
  720. "propName": "offsetDistance",
  721. "methodName": "offsetDistanceOffsetHandler"
  722. }, {
  723. "propName": "offsetSkidding",
  724. "methodName": "offsetSkiddingHandler"
  725. }, {
  726. "propName": "open",
  727. "methodName": "openHandler"
  728. }, {
  729. "propName": "placement",
  730. "methodName": "placementHandler"
  731. }, {
  732. "propName": "referenceElement",
  733. "methodName": "referenceElementHandler"
  734. }]; }
  735. }