UNPKG

@trimble-oss/moduswebcomponents

Version:

Modus Web Components is a modern, accessible UI library built with Stencil JS that provides reusable web components following Trimble's Modus design system. This updated version focuses on improved flexibility, enhanced theming options, comprehensive cust

396 lines (395 loc) 14 kB
import { createPopper } from "@popperjs/core"; import { h, Host, } from "@stencil/core"; import { inheritAriaAttributes } from "../utils"; /** * A customizable tooltip component used to create tooltips with different content. * * The tooltip can be dismissed by pressing the Escape key when hovering over it. * When forceOpen is enabled, the tooltip will remain open and can only be closed by setting forceOpen to false. */ export class ModusWcTooltip { constructor() { this.inheritedAttributes = {}; this.popperInstance = null; this.tooltipElement = null; this.triggerElement = null; /** The text content of the tooltip. */ this.content = ''; /** Custom CSS class to apply to the inner div. */ this.customClass = ''; /** Disables displaying the tooltip on hover */ this.disabled = false; /** The position that the tooltip will render in relation to the element. */ this.position = 'auto'; /** Track if tooltip was dismissed with Escape key */ this.escapeDismissed = false; /** Track if tooltip is currently visible */ this.isVisible = false; this.handleWindowResize = () => { if (this.popperInstance && this.isVisible) { void this.popperInstance.update(); } }; this.handleWindowScroll = () => { if (this.popperInstance && this.isVisible) { void this.popperInstance.update(); } }; } componentWillLoad() { this.inheritedAttributes = inheritAriaAttributes(this.el); } elementKeyupHandler(event) { switch (event.code) { case 'Escape': { // Allow Escape to dismiss tooltip when it's visible // When forceOpen is true, Escape should NOT dismiss it if (this.isVisible && !this.forceOpen) { this.escapeDismissed = true; this.dismissEscape.emit(); this.hideTooltip(); } break; } } } componentDidLoad() { this.triggerElement = this.el.querySelector('div > :first-child'); this.tooltipElement = document.createElement('div'); this.tooltipElement.className = `modus-wc-tooltip-content ${this.customClass || ''}`; this.tooltipElement.textContent = this.content; this.tooltipElement.setAttribute('role', 'tooltip'); if (this.tooltipId) { this.tooltipElement.id = this.tooltipId; } const arrow = document.createElement('div'); arrow.className = 'modus-wc-tooltip-arrow'; this.tooltipElement.appendChild(arrow); document.body.appendChild(this.tooltipElement); this.tooltipElement.style.display = 'none'; if (this.triggerElement && this.tooltipElement) { this.initializePopper(); } if (this.forceOpen && !this.disabled && !this.escapeDismissed) { this.showTooltip(); } } disconnectedCallback() { if (this.popperInstance) { this.popperInstance.destroy(); this.popperInstance = null; } if (this.tooltipElement && document.body.contains(this.tooltipElement)) { document.body.removeChild(this.tooltipElement); } window.removeEventListener('resize', this.handleWindowResize); window.removeEventListener('scroll', this.handleWindowScroll, true); } initializePopper() { if (!this.triggerElement || !this.tooltipElement) return; const placement = this.position === 'auto' ? 'top' : this.position; const arrowElement = this.tooltipElement.querySelector('.modus-wc-tooltip-arrow'); this.popperInstance = createPopper(this.triggerElement, this.tooltipElement, { placement, strategy: 'absolute', modifiers: [ { name: 'offset', options: { offset: [0, 8], }, }, { name: 'preventOverflow', options: { padding: 8, boundary: 'viewport', }, }, { name: 'flip', options: { fallbackPlacements: ['top', 'right', 'bottom', 'left'], padding: 8, boundary: 'viewport', }, }, { name: 'arrow', options: { element: arrowElement, padding: 5, }, }, { name: 'computeStyles', options: { adaptive: true, gpuAcceleration: true, }, }, { name: 'eventListeners', options: { scroll: true, resize: true, }, }, ], }); window.addEventListener('resize', this.handleWindowResize); window.addEventListener('scroll', this.handleWindowScroll, true); } showTooltip() { if (this.disabled || this.escapeDismissed || !this.tooltipElement) return; this.tooltipElement.style.display = 'block'; this.isVisible = true; if (this.popperInstance) { void this.popperInstance.update(); // Force a second update after a short delay to ensure arrow positioning setTimeout(() => { if (this.popperInstance) { void this.popperInstance.update(); } }, 10); } } hideTooltip() { if (!this.tooltipElement) return; if (!this.forceOpen || this.escapeDismissed) { this.tooltipElement.style.display = 'none'; this.isVisible = false; } } handlePositionChange() { if (this.popperInstance) { void this.popperInstance.setOptions({ placement: this.position === 'auto' ? 'top' : this.position, }); void this.popperInstance.update(); } } handleContentChange(newContent) { if (this.tooltipElement) { const arrow = this.tooltipElement.querySelector('.modus-wc-tooltip-arrow'); this.tooltipElement.textContent = newContent; if (arrow) { this.tooltipElement.appendChild(arrow); } } } handleForceOpenChange(forceOpen) { if (forceOpen && !this.disabled) { this.showTooltip(); } else { this.hideTooltip(); } } handleMouseEnter() { this.escapeDismissed = false; this.showTooltip(); } handleMouseLeave() { if (!this.forceOpen) { this.hideTooltip(); } } render() { return (h(Host, { key: '028f22dad1fdd4537bd012847d785b8d518d55b2' }, h("div", Object.assign({ key: '9638d7b4536e70c3a38ee3c7b81d27a69effd148', "aria-describedby": this.tooltipId, id: this.tooltipId }, this.inheritedAttributes), h("slot", { key: '326d59ea026f46496671e1eb4238ae59d6ec320d' })))); } static get is() { return "modus-wc-tooltip"; } static get originalStyleUrls() { return { "$": ["modus-wc-tooltip.scss"] }; } static get styleUrls() { return { "$": ["modus-wc-tooltip.css"] }; } static get properties() { return { "content": { "type": "string", "attribute": "content", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The text content of the tooltip." }, "getter": false, "setter": false, "reflect": false, "defaultValue": "''" }, "customClass": { "type": "string", "attribute": "custom-class", "mutable": false, "complexType": { "original": "string", "resolved": "string | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Custom CSS class to apply to the inner div." }, "getter": false, "setter": false, "reflect": false, "defaultValue": "''" }, "disabled": { "type": "boolean", "attribute": "disabled", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Disables displaying the tooltip on hover" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "false" }, "forceOpen": { "type": "boolean", "attribute": "force-open", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Use this attribute to force the tooltip to remain open." }, "getter": false, "setter": false, "reflect": false }, "tooltipId": { "type": "string", "attribute": "tooltip-id", "mutable": false, "complexType": { "original": "string", "resolved": "string | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "The ID of the tooltip element, useful for setting the \"aria-describedby\" attribute of related elements." }, "getter": false, "setter": false, "reflect": false }, "position": { "type": "string", "attribute": "position", "mutable": false, "complexType": { "original": "'auto' | 'top' | 'right' | 'bottom' | 'left'", "resolved": "\"auto\" | \"bottom\" | \"left\" | \"right\" | \"top\" | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "The position that the tooltip will render in relation to the element." }, "getter": false, "setter": false, "reflect": false, "defaultValue": "'auto'" } }; } static get states() { return { "escapeDismissed": {}, "isVisible": {} }; } static get events() { return [{ "method": "dismissEscape", "name": "dismissEscape", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "An event that fires when the tooltip is dismissed via Escape key" }, "complexType": { "original": "any", "resolved": "any", "references": {} } }]; } static get elementRef() { return "el"; } static get watchers() { return [{ "propName": "position", "methodName": "handlePositionChange" }, { "propName": "content", "methodName": "handleContentChange" }, { "propName": "forceOpen", "methodName": "handleForceOpenChange" }]; } static get listeners() { return [{ "name": "keyup", "method": "elementKeyupHandler", "target": "document", "capture": false, "passive": false }, { "name": "mouseenter", "method": "handleMouseEnter", "target": undefined, "capture": false, "passive": true }, { "name": "mouseleave", "method": "handleMouseLeave", "target": undefined, "capture": false, "passive": true }]; } }