UNPKG

@kit-data-manager/pid-component

Version:

The PID-Component is a web component that can be used to evaluate and display FAIR Digital Objects, PIDs, ORCiDs, and possibly other identifiers in a user-friendly way. It is easily extensible to support other identifier types.

362 lines (361 loc) 15.5 kB
/*! * * Copyright 2024-2026 Karlsruhe Institute of Technology. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ import { h, Host } from "@stencil/core"; export class PidTooltip { constructor() { this.isVisible = false; this.calculatedPosition = 'top'; this.needsRowExpansion = false; this.position = 'top'; this.maxWidth = '250px'; this.maxHeight = '150px'; this.fitContent = true; this.darkMode = 'light'; this.tooltipId = `tooltip-${Math.random().toString(36).substring(2, 11)}`; this.showTooltip = () => { this.isVisible = true; setTimeout(() => this.calculateOptimalPosition(), 10); }; this.hideTooltip = () => { this.isVisible = false; this.restoreRowHeight(); }; this.toggleTooltip = (event) => { event.preventDefault(); event.stopPropagation(); if (this.isVisible) { this.hideTooltip(); } else { this.showTooltip(); } }; this.handleButtonKeyDown = (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); this.toggleTooltip(event); } }; } handleKeyDown(event) { var _a; if (event.key === 'Escape' && this.isVisible) { this.hideTooltip(); (_a = this.buttonRef) === null || _a === void 0 ? void 0 : _a.focus(); event.preventDefault(); event.stopPropagation(); } } componentDidLoad() { this.calculatedPosition = this.position === 'bottom' ? 'bottom' : 'top'; this.tableRow = this.el.closest('tr'); } getIsDarkMode() { if (this.darkMode === 'dark') { return true; } if (this.darkMode === 'light') { return false; } const parentComponent = this.el.closest('pid-component'); if (parentComponent === null || parentComponent === void 0 ? void 0 : parentComponent.classList.contains('bg-gray-800')) { return true; } if (this.darkMode === 'system') { return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; } return false; } render() { const hasTooltipText = this.text && this.text.trim().length > 0; const buttonLabel = `${this.isVisible ? 'Hide' : 'Show'} additional information`; const isDarkMode = this.getIsDarkMode(); return (h(Host, { key: '2ec01611ee20170c269867cca75dcc0617cbeb8a', class: "relative inline-block w-full", onMouseEnter: this.showTooltip, onMouseLeave: this.hideTooltip }, this.isVisible && (h("span", { key: '71c1170e46db15c949c93747505810d0d998fe6d', class: "sr-only fixed", "aria-live": "assertive" }, "Information tooltip opened")), h("div", { key: '4abece66bd22d70d1f95a3f3fb8b36296e574f43', class: "flex items-center justify-between" }, h("slot", { key: '08b1030dd634d91583efdd09d856d2327daee49f', name: "trigger" }), hasTooltipText && (h("button", { key: '1d9834e041f1967f9539624d2275ca773e9b4e6a', ref: el => (this.buttonRef = el), type: "button", class: `flex items-center rounded-full p-0.5 transition-colors duration-200 ${isDarkMode ? 'text-gray-300 hover:bg-gray-700' : 'text-gray-600 hover:bg-gray-100'} focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 focus:outline-hidden`, "aria-label": buttonLabel, "aria-expanded": this.isVisible ? 'true' : 'false', "aria-controls": this.tooltipId, "aria-describedby": this.isVisible ? this.tooltipId : undefined, onClick: this.toggleTooltip, onKeyDown: this.handleButtonKeyDown, onFocus: this.showTooltip, onBlur: this.hideTooltip, tabIndex: 0, title: buttonLabel }, h("svg", { key: '843032e15d8bdc55d8d797d44a415025822b8f4d', "aria-hidden": "true", xmlns: "http://www.w3.org/2000/svg", class: "icon icon-tabler icon-tabler-info-circle min-h-4 min-w-4 shrink-0", width: "20", height: "20", viewBox: "0 0 24 24", "stroke-width": "1.5", stroke: "currentColor", fill: "none", "stroke-linecap": "round", "stroke-linejoin": "round", role: "img" }, h("title", { key: '07c313618f1bdaa3fecbcd15b8537d5b05921b2e' }, "Information icon"), h("desc", { key: 'bd491f016a8a78c1b42494dba7464589d59aab4d' }, "An icon indicating additional information is available"), h("path", { key: '52cbd7d3156efc50eb7d492b85a61663cf7136e4', stroke: "none", d: "M0 0h24v24H0z" }), h("circle", { key: '0ed085cb2563727f70970c249d27b56dbbed5401', cx: "12", cy: "12", r: "9" }), h("line", { key: 'da0d079ddeb7a943e4a613a569d8425f0d2cb214', x1: "12", y1: "8", x2: "12.01", y2: "8" }), h("polyline", { key: '061ef2bcd8d73b7966a3ecc1fa86a85495c94ac2', points: "11 12 12 12 12 16 13 16" }))))), hasTooltipText && (h("div", { key: '300f53d224ed53ec5056bbf50e69e59c201151b4', ref: el => (this.tooltipRef = el), id: this.tooltipId, role: "tooltip", class: `${this.isVisible ? 'block' : 'hidden'} absolute z-50 ${this.getPositionClasses(this.calculatedPosition)} w-full rounded border ${isDarkMode ? 'border-gray-600 bg-gray-700 text-gray-200' : 'border-gray-300 bg-white text-gray-700'} p-1 text-xs whitespace-normal shadow-lg transition-opacity duration-200 ease-in-out`, style: this.getTooltipStyles(), "aria-live": "polite" }, h("p", { key: '0f91843c7c982b4b992fdfc94c228d648830d8a4', class: "m-0 p-0" }, this.text))))); } calculateOptimalPosition() { if (!this.tooltipRef || !this.isVisible) return; const hostRect = this.el.getBoundingClientRect(); const table = this.el.closest('table'); const tableContainer = (table === null || table === void 0 ? void 0 : table.closest('.overflow-auto')) || (table === null || table === void 0 ? void 0 : table.parentElement); if (!tableContainer) { this.calculatedPosition = this.position === 'bottom' ? 'bottom' : 'top'; return; } const containerRect = tableContainer.getBoundingClientRect(); const spaceAbove = hostRect.top - containerRect.top; const spaceBelow = containerRect.bottom - hostRect.bottom; const tooltipHeight = this.estimateTooltipHeight(this.text); const requiredSpace = tooltipHeight + 20; let useBottom; let needsExpansion = false; if (this.position === 'top' && spaceAbove >= requiredSpace) { useBottom = false; } else if (this.position === 'bottom' && spaceBelow >= requiredSpace) { useBottom = true; } else if (spaceAbove >= spaceBelow && spaceAbove >= requiredSpace) { useBottom = false; } else if (spaceBelow >= requiredSpace) { useBottom = true; } else { useBottom = true; needsExpansion = true; } this.calculatedPosition = useBottom ? 'bottom' : 'top'; this.needsRowExpansion = needsExpansion; if (needsExpansion) { this.expandRowForTooltip(tooltipHeight); } } expandRowForTooltip(tooltipHeight) { if (!this.tableRow) return; this.originalRowHeight = this.tableRow.style.height || 'auto'; const currentRowHeight = this.tableRow.offsetHeight; const requiredAdditionalHeight = tooltipHeight + 40; const newRowHeight = Math.max(currentRowHeight, requiredAdditionalHeight); this.tableRow.style.transition = 'height 0.2s ease-in-out'; this.tableRow.style.height = `${newRowHeight}px`; this.tooltipExpansionChange.emit({ expand: true, requiredHeight: newRowHeight, }); } restoreRowHeight() { if (!this.tableRow || !this.needsRowExpansion) return; this.tableRow.style.height = this.originalRowHeight || 'auto'; setTimeout(() => { if (this.tableRow) { this.tableRow.style.transition = ''; } }, 200); this.needsRowExpansion = false; this.tooltipExpansionChange.emit({ expand: false, requiredHeight: 0, }); } estimateTooltipHeight(content) { const tempDiv = document.createElement('div'); tempDiv.setAttribute('aria-hidden', 'true'); tempDiv.setAttribute('tabindex', '-1'); tempDiv.setAttribute('role', 'presentation'); tempDiv.style.cssText = ` position: absolute; visibility: hidden; white-space: normal; word-wrap: break-word; max-width: ${this.maxWidth}; padding: 12px; font-size: 12px; line-height: 1.4; border: 1px solid transparent; pointer-events: none; `; tempDiv.textContent = content; document.body.appendChild(tempDiv); const height = tempDiv.offsetHeight; document.body.removeChild(tempDiv); return height; } getPositionClasses(position) { switch (position) { case 'top': return 'bottom-full left-0 mb-2'; case 'bottom': return 'top-full left-0 mt-2'; default: return 'top-full left-0 mt-2'; } } getTooltipStyles() { const styles = { maxWidth: this.maxWidth, }; if (this.needsRowExpansion || !this.fitContent) { styles.maxHeight = this.maxHeight; styles.overflowY = 'auto'; } return styles; } static get is() { return "pid-tooltip"; } static get properties() { return { "text": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": true, "optional": false, "docs": { "tags": [], "text": "The text to display in the tooltip" }, "getter": false, "setter": false, "reflect": false, "attribute": "text" }, "position": { "type": "string", "mutable": false, "complexType": { "original": "'top' | 'bottom'", "resolved": "\"bottom\" | \"top\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The preferred position of the tooltip (top or bottom)" }, "getter": false, "setter": false, "reflect": false, "attribute": "position", "defaultValue": "'top'" }, "maxWidth": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The maximum width of the tooltip" }, "getter": false, "setter": false, "reflect": false, "attribute": "max-width", "defaultValue": "'250px'" }, "maxHeight": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The maximum height of the tooltip" }, "getter": false, "setter": false, "reflect": false, "attribute": "max-height", "defaultValue": "'150px'" }, "fitContent": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Whether the tooltip should fit its content height exactly" }, "getter": false, "setter": false, "reflect": false, "attribute": "fit-content", "defaultValue": "true" }, "darkMode": { "type": "string", "mutable": false, "complexType": { "original": "'light' | 'dark' | 'system'", "resolved": "\"dark\" | \"light\" | \"system\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Dark mode setting for the tooltip.\nWhen provided, this takes precedence over DOM-based dark mode detection." }, "getter": false, "setter": false, "reflect": false, "attribute": "dark-mode", "defaultValue": "'light'" } }; } static get states() { return { "isVisible": {}, "calculatedPosition": {}, "needsRowExpansion": {} }; } static get events() { return [{ "method": "tooltipExpansionChange", "name": "tooltipExpansionChange", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Event emitted when tooltip requires row expansion" }, "complexType": { "original": "{ expand: boolean; requiredHeight: number }", "resolved": "{ expand: boolean; requiredHeight: number; }", "references": {} } }]; } static get elementRef() { return "el"; } static get listeners() { return [{ "name": "keydown", "method": "handleKeyDown", "target": undefined, "capture": false, "passive": false }]; } } //# sourceMappingURL=pid-tooltip.js.map