Skip to content

Commit bc05fa1

Browse files
authored
Merge pull request #179 from Genymobile/dev/player-103-refacto-default-turn-icon-and-tooltip
[PLAYER-103] refacto default turn icon and tooltip
2 parents 3d8ef03 + 0b18ae5 commit bc05fa1

7 files changed

Lines changed: 156 additions & 104 deletions

File tree

src/assets/images/ic_warning.svg

Lines changed: 3 additions & 1 deletion
Loading

src/plugins/PeerConnectionStats.js

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -73,44 +73,38 @@ export default class PeerConnectionStats {
7373
* Creates & display the default turn warning.
7474
*/
7575
displayDefaultTurnWarning() {
76-
// TODO look at this button and see if it can be improved
77-
let message = '<h1><span>&#9888;</span> Using a default TURN</h1>Performance is not optimal.';
78-
const li = document.createElement('li');
79-
const warning = document.createElement('div');
80-
warning.classList.add('gm-default-turn-button');
81-
warning.classList.add('gm-icon-button');
82-
const hover = document.createElement('div');
83-
hover.className = 'gm-default-turn-used gm-hidden';
84-
if (this.instance.options.connectionFailedURL) {
85-
message = message + '<br>Click on the icon to learn more.';
86-
warning.href = this.instance.options.connectionFailedURL;
87-
warning.target = '_blank';
88-
}
89-
hover.innerHTML = message;
90-
li.appendChild(hover);
91-
li.appendChild(warning);
92-
93-
warning.onmouseenter = () => {
94-
hover.classList.remove('gm-hidden');
95-
const {top} = warning.getBoundingClientRect();
96-
hover.style.top = `${top - hover.offsetHeight + hover.parentElement.offsetHeight / 2 + 10}px`;
97-
};
98-
warning.onmouseleave = () => {
99-
hover.classList.add('gm-hidden');
100-
};
76+
const turnButtonWarning = this.instance.toolbarManager.registerButton({
77+
id: 'default-turn-warning',
78+
iconClass: 'gm-default-turn-button gm-active',
79+
onClick: () => {
80+
if (this.instance.options.connectionFailedURL) {
81+
window.open(this.instance.options.connectionFailedURL, '_blank');
82+
}
83+
}
84+
});
10185

102-
const toolbar = this.instance.root.querySelector('.gm-toolbar ul');
103-
toolbar.appendChild(li);
86+
if (turnButtonWarning) {
87+
this.instance.toolbarManager.renderButton('default-turn-warning');
88+
let tooltipMsg = '⚠️ <b>Using a default TURN server. Performance is not optimal.</b>';
89+
if (this.instance.options.connectionFailedURL) {
90+
tooltipMsg = tooltipMsg + '<br><i>Click on the icon to learn more.</i>';
91+
}
92+
this.instance.tooltipManager.setTooltip(
93+
turnButtonWarning.htmlElement,
94+
tooltipMsg,
95+
this.instance.options.toolbarPosition === 'right' ? 'left' : 'right',
96+
null,
97+
true
98+
);
99+
}
100+
this.instance.toolbarManager.showButton('default-turn-warning');
104101
}
105102

106103
/**
107104
* Hide the default turn warning.
108105
*/
109106
hideDefaultTurnWarning() {
110-
const element = this.instance.getChildByClass(this.instance.root, 'gm-default-turn-button');
111-
if (element) {
112-
element.remove();
113-
}
107+
this.instance.toolbarManager.hideButton('default-turn-warning');
114108
}
115109

116110
/**

src/plugins/util/ToolBarManager.js

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,14 @@ export default class ToolbarManager {
121121
isInfloatingBar,
122122
});
123123

124-
this.instance.tooltipManager.setTooltip(
125-
button,
126-
title,
127-
isInfloatingBar ? 'top' : this.instance.options.toolbarPosition === 'right' ? 'left' : 'right',
128-
'toolbarTitleWidget',
129-
);
124+
if (title) {
125+
this.instance.tooltipManager.setTooltip(
126+
button,
127+
title,
128+
isInfloatingBar ? 'top' : this.instance.options.toolbarPosition === 'right' ? 'left' : 'right',
129+
'toolbarTitleWidget',
130+
);
131+
}
130132
}
131133

132134
/**
@@ -223,6 +225,34 @@ export default class ToolbarManager {
223225
buttonData.buttonIcon.classList.remove(className);
224226
}
225227

228+
/**
229+
* Show button.
230+
* @param {string} id - The ID of the button.
231+
*/
232+
showButton(id) {
233+
const buttonData = this.buttonRegistry.get(id);
234+
if (!buttonData || !buttonData.button) {
235+
log.warn(`No rendered button found with ID "${id}".`);
236+
return;
237+
}
238+
239+
buttonData.button.classList.remove('hidden');
240+
}
241+
242+
/**
243+
* Hide button.
244+
* @param {string} id - The ID of the button.
245+
*/
246+
hideButton(id) {
247+
const buttonData = this.buttonRegistry.get(id);
248+
if (!buttonData || !buttonData.button) {
249+
log.warn(`No rendered button found with ID "${id}".`);
250+
return;
251+
}
252+
253+
buttonData.button.classList.add('hidden');
254+
}
255+
226256
/**
227257
* setButtonActive - Set a button as active.
228258
* @param {string} id - The ID of the button.

src/plugins/util/TooltipManager.js

Lines changed: 88 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,17 @@ export default class TooltipManager {
3333
* @param {HTMLElement} element - The target element
3434
* @param {string} text - The tooltip text
3535
* @param {string} [position] - 'top'|'bottom'|'left'|'right' - Optional. Preferred position for the element. If not specified, the position will be determined in the following order of preference: 'bottom', 'left', 'right', 'top'.
36-
* @param {string} [classes] - Optional. Additional classes to add to the tooltip.
36+
* @param {string} [classes] - Optional. Additional classes to add to the tooltip.
37+
* @param {boolean} [isHTML] - Optional. If true, the tooltip text will be treated as HTML. ⚠️ SECURITY: This can lead to XSS vulnerabilities if the text is not sanitized or injected by users
3738
*/
38-
setTooltip(element, text, position, classes = null) {
39+
setTooltip(element, text, position, classes = null, isHTML = false) {
40+
// ⚠️ isHTML is used to display HTML content in the tooltip. It should be used with caution, especially if the text content is user-generated (translation, ...).
3941
if (!element) {
4042
return;
4143
}
4244

4345
this.removeTooltip(element); // Clean up first if already present
44-
const mouseEnter = () => this.showTooltip(element, text, position, classes);
46+
const mouseEnter = () => this.showTooltip(element, text, position, classes, isHTML);
4547
const mouseLeave = () => this.hideTooltip(classes);
4648

4749
element.gmTooltipListeners = {mouseEnter, mouseLeave};
@@ -68,14 +70,19 @@ export default class TooltipManager {
6870
* @param {string} [preferredPosition] - Preferred position ('top', 'bottom', 'left', 'right')
6971
*/
7072

71-
showTooltip(target, text, preferredPosition, classes) {
73+
showTooltip(target, text, preferredPosition, classes, isHTML) {
7274
if (classes) {
7375
classes.split(' ').forEach((cl) => {
7476
this.tooltipElement.classList.add(cl);
7577
});
7678
}
77-
this.tooltipElement.querySelector('.gm-tooltip-body').textContent = text;
79+
if (isHTML) {
80+
this.tooltipElement.querySelector('.gm-tooltip-body').innerHTML = text;
81+
} else {
82+
this.tooltipElement.querySelector('.gm-tooltip-body').textContent = text;
83+
}
7884
this.tooltipElement.classList.remove('top', 'bottom', 'left', 'right');
85+
this.resetArrowPosition();
7986

8087
const pos = this.computePosition(target, preferredPosition);
8188
this.tooltipElement.style.left = pos.left + 'px';
@@ -119,39 +126,70 @@ export default class TooltipManager {
119126
const rect = target.getBoundingClientRect();
120127
const tooltipRect = this.tooltipElement.getBoundingClientRect();
121128
const spacing = 10;
129+
const viewportWidth = window.innerWidth;
130+
const viewportHeight = window.innerHeight;
131+
const padding = 8;
132+
const maxLeft = Math.max(padding, viewportWidth - tooltipRect.width - padding);
133+
const maxTop = Math.max(padding, viewportHeight - tooltipRect.height - padding);
134+
// These functions ensure the tooltip stays within the viewport and adjust the arrow position accordingly
135+
const clampLeft = (left, position, top) => {
136+
const clamped = Math.min(Math.max(padding, left), maxLeft);
137+
if (clamped !== left) {
138+
this.updateArrowPosition(target, position, clamped, top);
139+
}
140+
return clamped;
141+
};
142+
const clampTop = (top, position, left) => {
143+
const clamped = Math.min(Math.max(padding, top), maxTop);
144+
if (clamped !== top) {
145+
this.updateArrowPosition(target, position, left, clamped);
146+
}
147+
return clamped;
148+
};
149+
// These functions check if the tooltip fits within the viewport
150+
const fitsHorizontally = (left) => left >= padding && left + tooltipRect.width <= viewportWidth - padding;
151+
const fitsVertically = (top) => top >= padding && top + tooltipRect.height <= viewportHeight - padding;
122152
const positions = preferred ? [preferred] : ['bottom', 'left', 'right', 'top'];
123153
for (const pos of positions) {
124154
let left, top;
125155
switch (pos) {
126156
case 'top': {
127157
left = rect.left + (rect.width - tooltipRect.width) / 2;
128158
top = rect.top - tooltipRect.height - spacing;
129-
if (top > 0) {
130-
return {left: Math.max(8, left), top, position: 'top'};
159+
if (fitsVertically(top)) {
160+
return {left: clampLeft(left, 'top', top), top: clampTop(top, 'top', left), position: 'top'};
131161
}
132162
break;
133163
}
134164
case 'left': {
135165
left = rect.left - tooltipRect.width - spacing;
136166
top = rect.top + (rect.height - tooltipRect.height) / 2;
137-
if (left > 0) {
138-
return {left, top: Math.max(8, top), position: 'left'};
167+
if (fitsHorizontally(left)) {
168+
return {left: clampLeft(left, 'left', top), top: clampTop(top, 'left', left), position: 'left'};
139169
}
140170
break;
141171
}
142172
case 'right': {
143173
left = rect.right + spacing;
144174
top = rect.top + (rect.height - tooltipRect.height) / 2;
145-
if (left + tooltipRect.width < window.innerWidth) {
146-
return {left, top: Math.max(8, top), position: 'right'};
175+
if (fitsHorizontally(left)) {
176+
return {
177+
left: clampLeft(left, 'right', top),
178+
top: clampTop(top, 'right', left),
179+
position: 'right',
180+
};
147181
}
148182
break;
149183
}
150184
case 'bottom': {
151185
left = rect.left + (rect.width - tooltipRect.width) / 2;
152186
top = rect.bottom + spacing;
153-
if (top + tooltipRect.height < window.innerHeight) {
154-
return {left: Math.max(8, left), top, position: 'bottom'};
187+
if (fitsVertically(top)) {
188+
return {
189+
left: clampLeft(left, 'bottom', top),
190+
top: clampTop(top, 'bottom', left),
191+
position: 'bottom',
192+
};
155193
}
156194
break;
157195
}
@@ -162,6 +200,42 @@ export default class TooltipManager {
162200
}
163201
const left = rect.left + (rect.width - tooltipRect.width) / 2;
164202
const top = rect.bottom + spacing;
165-
return {left: Math.max(8, left), top, position: 'bottom'};
203+
return {left: clampLeft(left, 'bottom', top), top: clampTop(top, 'bottom', left), position: 'bottom'};
204+
}
205+
206+
resetArrowPosition() {
207+
const arrow = this.tooltipElement.querySelector('.gm-tooltip-arrow');
208+
if (!arrow) {
209+
return;
210+
}
211+
arrow.style.removeProperty('--gm-tooltip-arrow-left');
212+
arrow.style.removeProperty('--gm-tooltip-arrow-top');
213+
}
214+
215+
updateArrowPosition(target, position, tooltipLeft, tooltipTop) {
216+
const arrow = this.tooltipElement.querySelector('.gm-tooltip-arrow');
217+
if (!arrow) {
218+
return;
219+
}
220+
221+
const targetRect = target.getBoundingClientRect();
222+
const tooltipRect = this.tooltipElement.getBoundingClientRect();
223+
const padding = 12;
224+
225+
if (position === 'top' || position === 'bottom') {
226+
const targetCenterX = targetRect.left + targetRect.width / 2;
227+
const tooltipLeftValue = typeof tooltipLeft === 'number' ? tooltipLeft : tooltipRect.left;
228+
const arrowLeft = targetCenterX - tooltipLeftValue;
229+
const clampedLeft = Math.min(Math.max(padding, arrowLeft), tooltipRect.width - padding);
230+
arrow.style.setProperty('--gm-tooltip-arrow-left', `${clampedLeft}px`);
231+
}
232+
233+
if (position === 'left' || position === 'right') {
234+
const targetCenterY = targetRect.top + targetRect.height / 2;
235+
const tooltipTopValue = typeof tooltipTop === 'number' ? tooltipTop : tooltipRect.top;
236+
const arrowTop = targetCenterY - tooltipTopValue;
237+
const clampedTop = Math.min(Math.max(padding, arrowTop), tooltipRect.height - padding);
238+
arrow.style.setProperty('--gm-tooltip-arrow-top', `${clampedTop}px`);
239+
}
166240
}
167241
}

src/scss/components/_tooltip.scss

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,31 +55,31 @@
5555
margin-bottom: -1 * $spacing-xs;
5656
.gm-tooltip-arrow {
5757
top: -8px;
58-
left: 50%;
58+
left: var(--gm-tooltip-arrow-left, 50%);
5959
transform: translateX(-50%) rotate(45deg);
6060
}
6161
}
6262
.gm-tooltip.top {
6363
margin-top: -1 * $spacing-xs;
6464
.gm-tooltip-arrow {
6565
bottom: -8px;
66-
left: 50%;
66+
left: var(--gm-tooltip-arrow-left, 50%);
6767
transform: translateX(-50%) rotate(45deg);
6868
}
6969
}
7070
.gm-tooltip.left {
7171
margin-left: -1 * $spacing-xs;
7272
.gm-tooltip-arrow {
7373
right: -8px;
74-
top: 50%;
74+
top: var(--gm-tooltip-arrow-top, 50%);
7575
transform: translateY(-50%) rotate(45deg);
7676
}
7777
}
7878
.gm-tooltip.right {
7979
margin-right: -1 * $spacing-xs;
8080
.gm-tooltip-arrow {
8181
left: -8px;
82-
top: 50%;
82+
top: var(--gm-tooltip-arrow-top, 50%);
8383
transform: translateY(-50%) rotate(45deg);
8484
}
8585
}

src/scss/components/_turn.scss

Lines changed: 0 additions & 47 deletions
This file was deleted.

src/scss/main.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
@use 'components/phone';
2020
@use 'components/baseband';
2121
@use 'components/clipboard';
22-
@use 'components/turn';
2322
@use 'components/fingerprints';
2423
@use 'components/fileUpload';
2524
@use 'components/slider';

0 commit comments

Comments
 (0)