forked from h5p/h5p-php-library
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
HFP-3548 Implement H5P.Tooltip (h5p#132)
* HFP-3548 Implement H5P.Tooltip * HFP-3548 Improve code and avoid duplication * HFP-3548 Implement configurable position for tooltip * HFP-3548 Add positional css to tooltip * HFP-3548 Change tooltip on aria-label change * HFP-3548 Add H5P.Tooltip support for Firefox * HFP-3548 Dynamically change options.text * HFP-3548 Remove unneccesary EventDispatcher * HFP-3548 Prevent propogation of clicks on tooltip * HFP-3548 Use document.body if in editor * HFP-3548 Fix universal state bug * HFP-3548 Remove dependency on known container * HFP-3548 Minor fixes * HFP-3548 Prevent overlap when adjusting * HFP-3548 Increase z-index for tooltip Co-authored-by: Paal Joergensen <[email protected]>
- Loading branch information
1 parent
25ee55b
commit 573d1ba
Showing
3 changed files
with
288 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
/*global H5P*/ | ||
H5P.Tooltip = (function () { | ||
'use strict'; | ||
|
||
/** | ||
* Create an accessible tooltip | ||
* | ||
* @param {HTMLElement} triggeringElement The element that should trigger the tooltip | ||
* @param {Object} options Options for tooltip | ||
* @param {String} options.text The text to be displayed in the tooltip | ||
* If not set, will attempt to set text = aria-label of triggeringElement | ||
* @param {String[]} options.classes Extra css classes for the tooltip | ||
* @param {Boolean} options.ariaHidden Whether the hover should be read by screen readers or not (default: true) | ||
* @param {String} options.position Where the tooltip should appear in relation to the | ||
* triggeringElement. Accepted positions are "top" (default), "left", "right" and "bottom" | ||
* | ||
* @constructor | ||
*/ | ||
function Tooltip(triggeringElement, options) { | ||
|
||
// Make sure tooltips have unique id | ||
H5P.Tooltip.uniqueId += 1; | ||
const tooltipId = 'h5p-tooltip-' + H5P.Tooltip.uniqueId; | ||
|
||
// Default options | ||
options = options || {}; | ||
options.classes = options.classes || []; | ||
options.ariaHidden = options.ariaHidden || true; | ||
|
||
// Initiate state | ||
let hover = false; | ||
let focus = false; | ||
|
||
// Function used by the escape listener | ||
const escapeFunction = function (e) { | ||
if (e.key === 'Escape') { | ||
tooltip.classList.remove('h5p-tooltip-visible'); | ||
} | ||
} | ||
|
||
// Create element | ||
const tooltip = document.createElement('div'); | ||
|
||
tooltip.classList.add('h5p-tooltip'); | ||
tooltip.id = tooltipId; | ||
tooltip.role = 'tooltip'; | ||
tooltip.innerHTML = options.text || triggeringElement.getAttribute('aria-label') || ''; | ||
tooltip.setAttribute('aria-hidden', options.ariaHidden); | ||
tooltip.classList.add(...options.classes); | ||
|
||
triggeringElement.appendChild(tooltip); | ||
|
||
// Set the initial position based on options.position | ||
switch (options.position) { | ||
case 'left': | ||
tooltip.classList.add('h5p-tooltip-left'); | ||
break; | ||
case 'right': | ||
tooltip.classList.add('h5p-tooltip-right'); | ||
break; | ||
case 'bottom': | ||
tooltip.classList.add('h5p-tooltip-bottom'); | ||
break; | ||
default: | ||
options.position = 'top'; | ||
} | ||
|
||
// Aria-describedby will override aria-hidden | ||
if (!options.ariaHidden) { | ||
triggeringElement.setAttribute('aria-describedby', tooltipId); | ||
} | ||
|
||
// Add event listeners to triggeringElement | ||
triggeringElement.addEventListener('mouseenter', function () { | ||
showTooltip(true); | ||
}); | ||
triggeringElement.addEventListener('mouseleave', function () { | ||
hideTooltip(true); | ||
}); | ||
triggeringElement.addEventListener('focusin', function () { | ||
showTooltip(false); | ||
}); | ||
triggeringElement.addEventListener('focusout', function () { | ||
hideTooltip(false); | ||
}); | ||
|
||
// Prevent clicks on the tooltip from triggering onClick listeners on the triggeringElement | ||
tooltip.addEventListener('click', function (event) { | ||
event.stopPropagation(); | ||
}); | ||
|
||
// Use a mutation observer to listen for aria-label being | ||
// changed for the triggering element. If so, update the tooltip. | ||
// Mutation observer will be used even if the original elements | ||
// doesn't have any aria-label. | ||
new MutationObserver(function (mutations) { | ||
const ariaLabel = mutations[0].target.getAttribute('aria-label'); | ||
if (ariaLabel) { | ||
tooltip.innerHTML = options.text || ariaLabel; | ||
} | ||
}).observe(triggeringElement, { | ||
attributes: true, | ||
attributeFilter: ['aria-label'], | ||
}); | ||
|
||
// Use intersection observer to adjust the tooltip if it is not completely visible | ||
new IntersectionObserver(function (entries) { | ||
entries.forEach((entry) => { | ||
const target = entry.target; | ||
const positionClass = 'h5p-tooltip-' + options.position; | ||
|
||
// Stop adjusting when hidden (to prevent a false positive next time) | ||
if (entry.intersectionRatio === 0) { | ||
['h5p-tooltip-down', 'h5p-tooltip-left', 'h5p-tooltip-right'] | ||
.forEach(function (adjustmentClass) { | ||
if (adjustmentClass !== positionClass) { | ||
target.classList.remove(adjustmentClass); | ||
} | ||
}); | ||
} | ||
// Adjust if not completely visible when meant to be | ||
else if (entry.intersectionRatio < 1 && (hover || focus)) { | ||
const targetRect = entry.boundingClientRect; | ||
const intersectionRect = entry.intersectionRect; | ||
|
||
// Going out of screen on left side | ||
if (intersectionRect.left > targetRect.left) { | ||
target.classList.add('h5p-tooltip-right'); | ||
target.classList.remove(positionClass); | ||
} | ||
// Going out of screen on right side | ||
else if (intersectionRect.right < targetRect.right) { | ||
target.classList.add('h5p-tooltip-left'); | ||
target.classList.remove(positionClass); | ||
} | ||
|
||
// going out of top of screen | ||
if (intersectionRect.top > targetRect.top) { | ||
target.classList.add('h5p-tooltip-down'); | ||
target.classList.remove(positionClass); | ||
} | ||
// going out of bottom of screen | ||
else if (intersectionRect.bottom < targetRect.bottom) { | ||
target.classList.add('h5p-tooltip-up'); | ||
target.classList.remove(positionClass); | ||
} | ||
} | ||
}); | ||
}).observe(tooltip); | ||
|
||
/** | ||
* Makes the tooltip visible and activates it's functionality | ||
* | ||
* @param {Boolean} triggeredByHover True if triggered by mouse, false if triggered by focus | ||
*/ | ||
const showTooltip = function (triggeredByHover) { | ||
if (triggeredByHover) { | ||
hover = true; | ||
} | ||
else { | ||
focus = true; | ||
} | ||
|
||
tooltip.classList.add('h5p-tooltip-visible'); | ||
|
||
// Add listener to iframe body, as esc keypress would not be detected otherwise | ||
document.body.addEventListener('keydown', escapeFunction, true); | ||
} | ||
|
||
/** | ||
* Hides the tooltip and removes listeners | ||
* | ||
* @param {Boolean} triggeredByHover True if triggered by mouse, false if triggered by focus | ||
*/ | ||
const hideTooltip = function (triggeredByHover) { | ||
if (triggeredByHover) { | ||
hover = false; | ||
} | ||
else { | ||
focus = false; | ||
} | ||
|
||
// Only hide tooltip if neither hovered nor focused | ||
if (!hover && !focus) { | ||
tooltip.classList.remove('h5p-tooltip-visible'); | ||
|
||
// Remove iframe body listener | ||
document.body.removeEventListener('keydown', escapeFunction, true); | ||
} | ||
} | ||
|
||
/** | ||
* Change the text displayed by the tooltip | ||
* | ||
* @param {String} text The new text to be displayed | ||
* Set to null to use aria-label of triggeringElement instead | ||
*/ | ||
this.setText = function (text) { | ||
options.text = text; | ||
tooltip.innerHTML = options.text || triggeringElement.getAttribute('aria-label') || ''; | ||
}; | ||
|
||
/** | ||
* Retrieve tooltip | ||
* | ||
* @return {HTMLElement} | ||
*/ | ||
this.getElement = function () { | ||
return tooltip; | ||
}; | ||
} | ||
|
||
return Tooltip; | ||
|
||
})(); | ||
|
||
H5P.Tooltip.uniqueId = -1; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
.h5p-tooltip { | ||
--translateX: -50%; | ||
--translateY: 0; | ||
|
||
display: none; | ||
position: absolute; | ||
bottom: 100%; | ||
left: 50%; | ||
transform: translateX(var(--translateX)) translateY(var(--translateY)); | ||
|
||
z-index: 4; | ||
|
||
font-size: 0.9rem; | ||
line-height: 1.5rem; | ||
|
||
padding: 0 0.5rem; | ||
white-space: nowrap; | ||
|
||
background: #000; | ||
color: #FFF; | ||
|
||
cursor: default; | ||
|
||
/* To hide the position adjustments and to get a bit more | ||
pleasent popup effect */ | ||
-webkit-animation: 800ms ease 0s normal forwards 1 fadein; | ||
animation: 800ms ease 0s normal forwards 1 fadein; | ||
} | ||
|
||
@keyframes fadein{ | ||
0% { opacity: 0; } | ||
80% { opacity: 0; } | ||
100% { opacity: 1; } | ||
} | ||
|
||
@-webkit-keyframes fadein{ | ||
0% { opacity: 0; } | ||
80% { opacity: 0; } | ||
100% { opacity: 1; } | ||
} | ||
|
||
.h5p-tooltip-bottom { | ||
top: 100%; | ||
bottom: auto; | ||
} | ||
|
||
.h5p-tooltip-left { | ||
--translateY: -50%; | ||
--translateX: 0; | ||
top: 50%; | ||
bottom: auto; | ||
left: auto; | ||
right: 100%; | ||
} | ||
|
||
.h5p-tooltip-right { | ||
--translateY: -50%; | ||
--translateX: 0; | ||
top: 50%; | ||
bottom: auto; | ||
left: 100%; | ||
right: auto; | ||
} | ||
|
||
.h5p-tooltip-visible { | ||
display: block; | ||
} |