UNPKG

@financial-times/o3-tooltip

Version:

Provides a viewport-aware tooltip for annotating or or highlighting other aspects of a product's UI

288 lines (285 loc) 8.19 kB
// src/ts/tooltip.ts import { createPopper } from "@popperjs/core"; var ToolTip = class extends HTMLElement { constructor() { super(); } static get observedAttributes() { return ["placement", "title", "content"]; } get placement() { return this.getAttribute("placement"); } set placement(value) { this.setAttribute("placement", value); } get title() { return this.getAttribute("title"); } set title(value) { this.setAttribute("title", value); } get content() { return this.getAttribute("content"); } set content(value) { this.setAttribute("content", value); } connectedCallback() { this.content = this.getAttribute("content"); if (this.hasAttribute("title")) { this.title = this.getAttribute("title"); } this.contentId = this.getAttribute("content-id"); this._mutationObserver?.disconnect(); this._resizeObserver?.disconnect(); const contentRoot = this.shadowRoot?.querySelector('[part="content"]') ?? this; this._mutationObserver = new MutationObserver(() => this.update()); this._mutationObserver.observe(contentRoot, { childList: true, subtree: true, characterData: true }); this._resizeObserver = new ResizeObserver(() => this.update()); this._resizeObserver.observe(this); } attributeChangedCallback(name) { this.render(name); } disconnectedCallback() { this._popperInstance?.destroy(); this._popperInstance = void 0; this._mutationObserver?.disconnect(); this._resizeObserver?.disconnect(); } async update() { await this._popperInstance?.update(); } forceUpdate() { this._popperInstance?.forceUpdate?.(); } render(name) { if (this._popperInstance) { this._popperInstance.setOptions((opts) => ({ ...opts, placement: this.placement })); } if (name === "title" || name === "content") { const titleEl = this.querySelector(".o3-tooltip-content-title"); const contentEl = this.querySelector( ".o3-tooltip-content-body" ); if (titleEl) { titleEl.textContent = this.title; } if (contentEl) { contentEl.textContent = this.content; } if (!this.title) { this.setAttribute("no-title", ""); } else { this.removeAttribute("no-title"); } this.forceUpdate(); } if (name && name !== "title" && name !== "content") { void this.update(); } } initialisePopper(targetNode, popperElement) { this._popperInstance = createPopper(targetNode, popperElement, { placement: this.placement || "top", modifiers: [ { name: "preventOverflow", options: { rootBoundary: document.body } }, { name: "eventListeners", options: { scroll: false } }, { name: "flip", options: { fallbackPlacements: ["top", "bottom", "left", "right"], rootBoundary: document.body } }, { name: "offset", options: { offset: [0, 16] } } ] }); return this._popperInstance; } }; // src/ts/onboardingTooltip.ts var OnboardingToolTip = class extends ToolTip { constructor() { super(...arguments); this._clickHandler = () => { this._onClose?.(); this.remove(); }; } connectedCallback() { super.connectedCallback(); this.innerHTML = this._generateMarkup( this.title, this.content, this.contentId ); this._targetNode = this.getTargetNode(); this._popperInstance = this.initialisePopper(this._targetNode, this); this._closeButton = this.querySelector(".o3-tooltip-close"); this._addEventListeners(); } disconnectedCallback() { super.disconnectedCallback(); this._removeEventListeners(); } _addEventListeners() { this._closeButton.addEventListener("click", this._clickHandler); } _removeEventListeners() { this._closeButton.removeEventListener("click", this._clickHandler); } getTargetNode() { this.targetId = this.getAttribute("target-id"); let targetNode = document.getElementById(this.targetId); if (!targetNode) { throw new Error( "Target node not found. o3-tooltip requires a target node id to position itself against the target element." ); } return targetNode; } set onClose(callback) { this._onClose = callback; } _generateMarkup(title, content, contentId) { return ` <div class="o3-tooltip-wrapper"> <div data-tooltip-arrow></div> <div class="o3-tooltip-content" id=${contentId}> <div class="o3-tooltip-content-title">${title}</div> <div class="o3-tooltip-content-body">${content}</div> </div> <button type="button" class="o3-tooltip-close" aria-label="Close tooltip" title="Close tooltip"></button> </div> `; } }; if (!customElements.get("o3-tooltip-onboarding")) { customElements.define("o3-tooltip-onboarding", OnboardingToolTip); } // src/ts/toggleTooltip.ts var ToggleToolTip = class extends ToolTip { constructor() { super(); this._clickHandler = (e) => { if (this._contentWrapper.style.display === "none") { e.stopPropagation(); this._addContentInLiveRegion(); return; } this._cleanUp(); }; this._closeOnOutsideClick = (e) => { const target = e.target; const isTarget = target === this._targetNode; const isChild = this.contains(target); if (!isChild && !isTarget) { this._cleanUp(); } }; this._closeOnEsc = (e) => { if (e.key === "Escape") { this._cleanUp(); } }; this.infoLabel = "more information"; } connectedCallback() { super.connectedCallback(); if (this.getAttribute("info-label")) { this.infoLabel = this.getAttribute("info-label"); } this.innerHTML = this._generateMarkup(this.infoLabel); this._contentWrapper = this.querySelector( ".o3-tooltip-wrapper" ); this._liveRegionEl = this.querySelector( ".o3-tooltip-content" ); this._contentWrapper.style.display = "none"; this._targetNode = this.querySelector( ".o3-toggletip-target" ); this._popperInstance = this.initialisePopper( this._targetNode, this._contentWrapper ); this._addEventListeners(); } disconnectedCallback() { super.disconnectedCallback(); this._removeEventListeners(); } _cleanUp() { this._contentWrapper.style.display = "none"; this.removeAttribute("open"); this._removeContentInLiveRegion(); } _addContentInLiveRegion() { this._liveRegionEl.innerHTML = ""; this._contentWrapper.style.display = "block"; this._contentWrapper.style.opacity = "0"; this.setAttribute("open", ""); setTimeout(() => { this.render(); this._liveRegionEl.innerHTML = ` <div class="o3-tooltip-content-title">${this.title}</div> <div class="o3-tooltip-content-body">${this.content}</div> `; this._contentWrapper.style.opacity = "1"; }, 100); } _removeContentInLiveRegion() { this._liveRegionEl.innerHTML = ""; } _addEventListeners() { this._targetNode.addEventListener("click", this._clickHandler); document.addEventListener("click", this._closeOnOutsideClick); this.addEventListener("keydown", this._closeOnEsc); } _removeEventListeners() { this._targetNode.removeEventListener("click", this._clickHandler); document.removeEventListener("click", this._closeOnOutsideClick); this.removeEventListener("keydown", this._closeOnEsc); } _generateMarkup(infoLabel) { const tooltipButtonMarkup = `<button type="button" aria-label="${infoLabel}" class="o3-toggletip-target"></button>`; return ` ${tooltipButtonMarkup} <div class="o3-tooltip-wrapper"> <div data-tooltip-arrow></div> <div class="o3-tooltip-content" role="status"></div> </div>`; } }; if (!customElements.get("o3-tooltip-toggle")) { customElements.define("o3-tooltip-toggle", ToggleToolTip); }