@@ -5,7 +5,7 @@ interface SmartTooltipOptions {
55}
66
77interface Position {
8- name : 'top' | 'bottom' | 'left' | 'right'
8+ name : 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
99 x : number
1010 y : number
1111}
@@ -14,6 +14,42 @@ const defaultOptions = {
1414 triggerName : 'tooltip' ,
1515}
1616
17+ /**
18+ * The `SmartTooltip` class provides functionality to display tooltips on HTML elements.
19+ * Tooltips can be triggered by mouse hover or click events and are positioned optimally
20+ * within the viewport to avoid overflow.
21+ *
22+ * @example
23+ * ```typescript
24+ * const tooltip = new SmartTooltip({
25+ * triggerName: 'tooltip'
26+ * });
27+ * ```
28+ *
29+ * @remarks
30+ * The tooltip content is specified using a data attribute on the trigger element.
31+ * The tooltip can be triggered by elements with the specified `triggerName` data attribute.
32+ *
33+ * @param {SmartTooltipOptions } options - Configuration options for the tooltip.
34+ *
35+ * @property {string } triggerName - The name of the data attribute used to trigger the tooltip.
36+ * @property {HTMLDivElement } tooltip - The tooltip element.
37+ * @property {string | null } activeTriggerType - The type of the currently active trigger ('click' or 'hover').
38+ * @property {number } spacing - The spacing between the tooltip and the trigger element.
39+ *
40+ * @method setupEventListeners - Sets up event listeners for mouseover, mouseout, click, resize, and scroll events.
41+ * @method handleClick - Handles click events to show or hide the tooltip.
42+ * @method handleMouseOver - Handles mouseover events to show the tooltip.
43+ * @method handleMouseOut - Handles mouseout events to hide the tooltip.
44+ * @method handleResize - Handles window resize events to hide the tooltip.
45+ * @method handleScroll - Handles window scroll events to hide the tooltip.
46+ * @method isVisible - Checks if the tooltip is currently visible.
47+ * @method calculatePosition - Calculates the optimal position for the tooltip relative to the trigger element.
48+ * @method fitsInViewport - Checks if the tooltip fits within the viewport and is not obstructed by other elements.
49+ * @method show - Displays the tooltip with the specified content.
50+ * @method hide - Hides the tooltip.
51+ * @method destroy - Removes event listeners and the tooltip element from the DOM.
52+ */
1753export class SmartTooltip {
1854 readonly triggerName : string
1955 private readonly tooltip : HTMLDivElement
@@ -23,7 +59,7 @@ export class SmartTooltip {
2359 constructor ( options : SmartTooltipOptions = defaultOptions ) {
2460 this . triggerName = `data-${ options . triggerName } `
2561 this . tooltip = document . createElement ( 'div' )
26- this . tooltip . className = styles . tooltip
62+ this . tooltip . className = `d-tooltip ${ styles . tooltip } `
2763 document . body . appendChild ( this . tooltip )
2864
2965 this . setupEventListeners ( )
@@ -89,6 +125,14 @@ export class SmartTooltip {
89125 return this . tooltip . classList . contains ( styles . visible )
90126 }
91127
128+ /**
129+ * Calculates the optimal position for the tooltip relative to the trigger element.
130+ * It tries to find a position where the tooltip fits within the viewport.
131+ * If no position fits, it defaults to the first position in the list.
132+ *
133+ * @param {HTMLElement } trigger - The HTML element that triggers the tooltip.
134+ * @returns {Position } The calculated position for the tooltip.
135+ */
92136 private calculatePosition ( trigger : HTMLElement ) : Position {
93137 const triggerRect = trigger . getBoundingClientRect ( )
94138 const tooltipRect = this . tooltip . getBoundingClientRect ( )
@@ -114,18 +158,74 @@ export class SmartTooltip {
114158 x : triggerRect . right + this . spacing ,
115159 y : triggerRect . top + ( triggerRect . height - tooltipRect . height ) / 2 ,
116160 } ,
161+ // Corner positions
162+ {
163+ name : 'top-left' ,
164+ x : triggerRect . left ,
165+ y : triggerRect . top - tooltipRect . height - this . spacing ,
166+ } ,
167+ {
168+ name : 'top-right' ,
169+ x : triggerRect . right - tooltipRect . width ,
170+ y : triggerRect . top - tooltipRect . height - this . spacing ,
171+ } ,
172+ {
173+ name : 'bottom-left' ,
174+ x : triggerRect . left ,
175+ y : triggerRect . bottom + this . spacing ,
176+ } ,
177+ {
178+ name : 'bottom-right' ,
179+ x : triggerRect . right - tooltipRect . width ,
180+ y : triggerRect . bottom + this . spacing ,
181+ } ,
117182 ]
118183
119184 return positions . find ( pos => this . fitsInViewport ( pos , tooltipRect ) ) || positions [ 0 ]
120185 }
121186
187+ /**
188+ * Checks if the tooltip fits within the viewport and is not obstructed by other elements.
189+ *
190+ * @param pos - The position of the tooltip.
191+ * @param tooltipRect - The bounding rectangle of the tooltip.
192+ * @returns `true` if the tooltip fits within the viewport and is not obstructed, otherwise `false`.
193+ */
122194 private fitsInViewport ( pos : Position , tooltipRect : DOMRect ) : boolean {
123- return (
195+ // First check if tooltip is within viewport bounds
196+ const inViewport =
124197 pos . x >= 0 &&
125198 pos . y >= 0 &&
126199 pos . x + tooltipRect . width <= window . innerWidth &&
127200 pos . y + tooltipRect . height <= window . innerHeight
128- )
201+
202+ if ( ! inViewport ) return false
203+
204+ // Check if tooltip is obstructed by other elements
205+ const points = [
206+ [ pos . x , pos . y ] , // Top-left
207+ [ pos . x + tooltipRect . width , pos . y ] , // Top-right
208+ [ pos . x , pos . y + tooltipRect . height ] , // Bottom-left
209+ [ pos . x + tooltipRect . width , pos . y + tooltipRect . height ] , // Bottom-right
210+ [ pos . x + tooltipRect . width / 2 , pos . y + tooltipRect . height / 2 ] , // Center
211+ ]
212+
213+ // Get all elements at these points
214+ const elementsAtPoints = points . flatMap ( ( [ x , y ] ) => Array . from ( document . elementsFromPoint ( x , y ) ) )
215+
216+ // Filter out non-relevant elements
217+ const obstructingElements = elementsAtPoints . filter ( element => {
218+ if (
219+ this . tooltip . contains ( element ) || // Exclude tooltip and its children
220+ element === this . tooltip ||
221+ element . classList . contains ( styles . tooltip ) || // Ignore other tooltips
222+ getComputedStyle ( element ) . pointerEvents === 'none' // Ignore non-interactive elements
223+ ) {
224+ return false
225+ }
226+ } )
227+
228+ return obstructingElements . length === 0
129229 }
130230
131231 private show ( trigger : HTMLElement , content : string | null ) {
0 commit comments