@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
JavaScript
/*!
*
* 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