@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
469 lines (468 loc) • 17.1 kB
JavaScript
import { createPopper } from "@popperjs/core";
import { h, Host, } from "@stencil/core";
import { handleShadowDOMStyles } from "../base-component";
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.
* Use the contentElement prop to supply rich HTML content to the tooltip such as multiline text.
* For plain dynamic text, prefer the content prop instead. When contentElement is set, it takes precedence over the content prop.
*/
export class ModusWcTooltip {
constructor() {
this.inheritedAttributes = {};
this.popperInstance = null;
this.tooltipElement = null;
this.triggerElement = null;
/** The text content of the tooltip. When contentElement is also set, contentElement takes precedence. */
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();
}
};
}
handlePositionChange() {
if (this.popperInstance) {
void this.popperInstance.setOptions({
placement: this.position === 'auto' ? 'top' : this.position,
});
void this.popperInstance.update();
}
}
handleContentChange() {
if (this.contentElement)
return;
this.applyContentToTooltip();
}
handleContentElementChange() {
this.applyContentToTooltip();
}
handleForceOpenChange(forceOpen) {
if (forceOpen && !this.disabled) {
this.showTooltip();
}
else {
this.hideTooltip();
}
}
componentWillLoad() {
handleShadowDOMStyles(this.el);
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.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);
this.tooltipElement.setAttribute('popover', 'manual');
this.applyContentToTooltip();
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) {
if (typeof this.tooltipElement.hidePopover === 'function') {
try {
this.tooltipElement.hidePopover();
}
catch (_a) {
// Already hidden or element not connected
}
}
if (this.tooltipElement.parentElement) {
this.tooltipElement.parentElement.removeChild(this.tooltipElement);
}
}
window.removeEventListener('resize', this.handleWindowResize);
window.removeEventListener('scroll', this.handleWindowScroll, true);
}
/** Precedence: contentElement (rich HTML) → content (plain string). Arrow is always kept last. */
applyContentToTooltip() {
if (!this.tooltipElement)
return;
const arrow = this.tooltipElement.querySelector('.modus-wc-tooltip-arrow');
Array.from(this.tooltipElement.childNodes).forEach((node) => {
if (node !== arrow) {
this.tooltipElement.removeChild(node);
}
});
if (this.contentElement && 'nodeType' in this.contentElement) {
this.tooltipElement.insertBefore(this.contentElement.cloneNode(true), arrow);
}
else {
this.tooltipElement.insertBefore(document.createTextNode(this.content), arrow);
}
}
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: 'fixed',
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';
if (typeof this.tooltipElement.showPopover === 'function') {
try {
this.tooltipElement.showPopover();
}
catch (_a) {
// Already showing or element not connected
}
}
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) {
if (typeof this.tooltipElement.hidePopover === 'function') {
try {
this.tooltipElement.hidePopover();
}
catch (_a) {
// Already hidden or element not connected
}
}
this.tooltipElement.style.display = 'none';
this.isVisible = false;
}
}
handleMouseEnter() {
this.escapeDismissed = false;
this.showTooltip();
}
handleMouseLeave() {
if (!this.forceOpen) {
this.hideTooltip();
}
}
render() {
return (h(Host, { key: 'cce7d08b0e748ee680f15744b442655d7a32c13e' }, h("div", Object.assign({ key: '7a384a1329786729aeee04a689f88b16b2951808', "aria-describedby": this.tooltipId, id: this.tooltipId }, this.inheritedAttributes), h("slot", { key: '13ef7dbaae4806f15d8dcb21ee5590b7e838504c' }))));
}
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. When contentElement is also set, contentElement takes precedence."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "''"
},
"contentElement": {
"type": "unknown",
"attribute": "content-element",
"mutable": false,
"complexType": {
"original": "HTMLElement",
"resolved": "HTMLElement | undefined",
"references": {
"HTMLElement": {
"location": "global",
"id": "global::HTMLElement"
}
}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "An optional rich HTML element to render as the tooltip body.\nWhen set, this takes precedence over the `content` string prop.\nThe element is deep-cloned into the tooltip container."
},
"getter": false,
"setter": false
},
"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": "contentElement",
"methodName": "handleContentElementChange"
}, {
"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
}];
}
}