Skip to content
Snippets Groups Projects
Commit 062a7f9a authored by Moritz Strohm's avatar Moritz Strohm
Browse files

fixed screen reader output for tooltips, fixes #3962

Closes #3962

Merge request studip/studip!2859
parent d28f3aff
No related branches found
No related tags found
No related merge requests found
......@@ -596,7 +596,12 @@ function tooltipIcon($text, $important = false, $html = false): string
// render tooltip
$template = $GLOBALS['template_factory']->open('shared/tooltip');
return $template->render(compact('text', 'important', 'html'));
return $template->render([
'text' => $text,
'important' => $important,
'html' => $html,
'tooltip_id' => md5($text)
]);
}
/**
......@@ -608,9 +613,13 @@ function tooltipIcon($text, $important = false, $html = false): string
function tooltipHtmlIcon($text, $important = false)
{
// render tooltip
$html = true;
$template = $GLOBALS['template_factory']->open('shared/tooltip');
return $template->render(compact('text', 'important', 'html'));
return $template->render([
'text' => $text,
'important' => $important,
'html' => true,
'tooltip_id' => md5($text)
]);
}
/**
......
// Attach global hover handler for tooltips.
// Applies to all elements having a "data-tooltip" attribute.
// Tooltip may be provided in the data-attribute itself or by
// defining a title attribute. The latter is prefered due to
// the obvious accessibility issues.
var timeout = null;
STUDIP.Tooltip.threshold = 6;
$(document).on('mouseenter mouseleave focusin focusout', '[data-tooltip],.tooltip:has(.tooltip-content)', function(event) {
let data = $(this).data();
const visible = event.type === 'mouseenter' || event.type === 'focusin';
const offset = $(this).offset();
const x = offset.left + $(this).outerWidth(true) / 2;
const y = offset.top;
const delay = data.tooltipDelay ?? 300;
let content;
let tooltip;
if (!data.tooltipObject) {
// If tooltip has not yet been created (first hover), obtain it's
// contents and create the actual tooltip object.
if (!data.tooltip || !$.isPlainObject(data.tooltip)) {
content = $('<div/>').text(data.tooltip || $(this).attr('title')).html();
} else if (data.tooltip.html !== undefined) {
content = data.tooltip.html;
} else if (data.tooltip.text !== undefined) {
content = data.tooltip.text;
} else {
throw "Invalid content for tooltip via data";
}
if (!content) {
content = $(this).find('.tooltip-content').remove().html();
}
$(this).attr('title', null);
$(this).attr('data-tooltip', content);
tooltip = new STUDIP.Tooltip(x, y, content);
data.tooltipObject = tooltip;
$(this).attr('aria-describedby', tooltip.id);
$(this).on('remove', function() {
tooltip.remove();
});
} else if (visible) {
// If tooltip has already been created, update it's position.
// This is neccessary if the surrounding content is scrollable AND has
// been scrolled. Otherwise the tooltip would appear at it's previous
// and now wrong location.
data.tooltipObject.position(x, y);
}
if (visible) {
$('.studip-tooltip').not(data.tooltipObject).hide();
data.tooltipObject.show();
} else {
timeout = setTimeout(() => data.tooltipObject.hide(), delay);
}
}).on('mouseenter focusin', '.studip-tooltip', () => {
clearTimeout(timeout);
}).on('mouseleave focusout', '.studip-tooltip', function() {
$(this).hide();
});
......@@ -55,7 +55,6 @@ import "./bootstrap/article.js"
import "./bootstrap/copyable_links.js"
import "./bootstrap/selection.js"
import "./bootstrap/data_secure.js"
import "./bootstrap/tooltip.js"
import "./bootstrap/lightbox.js"
import "./bootstrap/application.js"
import "./bootstrap/global_search.js"
......
......@@ -76,7 +76,6 @@ import Statusgroups from './lib/statusgroups.js';
import study_area_selection from './lib/study_area_selection.js';
import Table from './lib/table.js';
import TableOfContents from './lib/table-of-contents.js';
import Tooltip from './lib/tooltip.js';
import Tour from './lib/tour.js';
import * as Gettext from './lib/gettext';
import UserFilter from './lib/user_filter.js';
......@@ -164,7 +163,6 @@ window.STUDIP = _.assign(window.STUDIP || {}, {
study_area_selection,
Table,
TableOfContents,
Tooltip,
Tour,
URLHelper,
UserFilter,
......
import CSS from './css.js';
/**
* Tooltip library for Stud.IP
*
* @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
* @copyright Stud.IP Core Group 2014
* @license GPL2 or any later version
* @since Stud.IP 3.1
*/
let count = 0;
let threshold = 0;
class Tooltip {
static get count() {
return count;
}
static set count(value) {
count = value;
}
// Threshold used for "edge detection" (imagine a padding along the edges)
static get threshold() {
return threshold;
}
static set threshold(value) {
threshold = value;
}
/**
* Returns a new unique id of a tooltip.
*
* @return {string} Unique id
* @static
*/
static getId() {
const id = `studip-tooltip-${Tooltip.count}`;
Tooltip.count += 1;
return id;
}
/**
* Constructs a new tooltip at given location with given content.
* The applied css class may be changed by the fourth parameter.
*
* @class
* @classdesc Stud.IP tooltips provide an improved layout and handling
* of contents (including html) than the browser's default
* tooltip through title attribute would
*
* @param {int} x - Horizontal position of the tooltip
* @param {int} y - Vertical position of the tooltip
* @param {string} content - Content of the tooltip (may be html)
* @param {string} css_class - Optional name of the applied css class /
* defaults to 'studip-tooltip'
*/
constructor(x, y, content, css_class) {
// Obtain unique id of the tooltip
this.id = Tooltip.getId();
// Create dom element of the tooltip, apply id and class and attach
// to dom
this.element = $('<div>');
this.element.addClass(css_class || 'studip-tooltip');
this.element.attr('id', this.id);
this.element.attr('role', 'tooltip');
this.element.appendTo('body');
// Set position and content and paint the tooltip
this.position(x, y);
this.update(content);
this.paint();
}
/**
* Translates the arrow(s) under a tooltip using css3 translate
* transforms. This is needed at the edges of the screen.
* This implies that a current browser is used. The translation could
* also be achieved by adjusting margins but that way we would need
* to hardcode values into this function since it's a struggle to
* obtain the neccessary values from the CSS pseudo selectors in JS.
*
* Internal, css rules are dynamically created and applied to the current
* document by using the methods provided in the file studip-css.js.
*
* @param {int} x - Horizontal offset
* @param {int} y - Vertical offset
*/
translateArrows(x, y, left_arrow = false) {
CSS.removeRule(`#${this.id}::before`);
CSS.removeRule(`#${this.id}::after`);
if (x !== 0 || y !== 0) {
let before_rule = {
transform: `translate(${x}px, ${y}px);`
};
if (left_arrow) {
before_rule.transform = `translate(${x}px, ${y}px) rotate(90deg);`;
}
let after_rule = before_rule;
if (left_arrow) {
after_rule['border-width'] = '9px';
}
CSS.addRule(`#${this.id}::before`, before_rule, ['-ms-', '-webkit-']);
CSS.addRule(`#${this.id}::after`, after_rule, ['-ms-', '-webkit-']);
}
}
/**
* Updates the position of the tooltip.
*
* @param {int} x - Horizontal position of the tooltip
* @param {int} y - Vertical position of the tooltip
*/
position(x, y) {
this.x = x;
this.y = y;
}
/**
* Updates the contents of the tooltip.
*
* @param {string} content - Content of the tooltip (may be html)
*/
update(content) {
this.element.html(content);
}
/**
* "Paints" the tooltip. This method actually computes the dimensions of
* the tooltips, checks for screen edges and calculates the actual offset
* in the current document.
* This method is neccessary due to the fact that position and content
* can be changed apart from each other.
* Thus: Don't forget to repaint after adjusting any of the two.
*/
paint() {
const width = this.element.outerWidth(true);
const height = this.element.outerHeight(true);
const maxWidth = $(document).width();
const maxHeight = $(document).height();
let x = this.x - width / 2;
let y = this.y - height;
//The arrow offset is the offset from the bottom right corner of
//the tooltip "frame".
let arrow_offset_x = 0;
let arrow_offset_y = 0;
let left_arrow = false;
if (y < 0) {
y = 0;
x = this.x + 20;
//Put the arrow on the left side and move the tooltip,
//if there is still enough place left on the right.
left_arrow = true;
arrow_offset_y = -height + this.y + 10;
if (arrow_offset_y > -20) {
y+= arrow_offset_y + 20;
arrow_offset_y = -20;
}
arrow_offset_x = -width / 2 - 8;
} else if (y + height > maxHeight) {
y = maxHeight - height;
}
if (x < 0) {
arrow_offset_x = 0;
x = 0;
} else if (x + width > maxWidth) {
arrow_offset_x = x + width - maxWidth;
x = maxWidth - width;
}
this.translateArrows(arrow_offset_x, arrow_offset_y, left_arrow);
this.element.css({
left: x,
top: y
});
}
/**
* Toggles the visibility of the tooltip. If no state is provided,
* the tooltip will be hidden if visible and vice versa. Pretty straight
* forward and no surprises here.
* This method implicitely calls paint before a tooltip is shown (in case
* it was forgotten).
*
* @param {bool} visible - Optional visibility parameter to set the
* tooltip to a certain state
*/
toggle(visible) {
if (visible) {
this.paint();
}
this.element.toggle(visible);
}
/**
* Reveals the tooltip.
*
* @see Tooltip.toggle
*/
show() {
this.toggle(true);
}
/**
* Hides the tooltip.
*
* @see Tooltip.toggle
*/
hide() {
this.toggle(false);
}
/**
* Removes the tooltip
*/
remove() {
this.element.remove();
}
}
export default Tooltip;
......@@ -7,7 +7,7 @@
box-shadow: 0 1px 0 fade-out($white, 0.5) inset;
font-size: var(--font-size-base);
margin-bottom: 8px;
max-width: 230px;
max-width: $grid-element-width;
padding: 10px;
position: absolute;
text-align: left;
......@@ -38,11 +38,13 @@
@extend %tooltip;
display: none;
}
&:hover .tooltip-content {
&:hover .tooltip-content,
&:focus .tooltip-content {
bottom: 100%;
display: inline-block;
left: 50%;
margin-left: -129px;
width: 230px;
margin-left: - calc($grid-element-width / 2) - 10px;
width: $grid-element-width;
}
}
<span class="tooltip tooltip-icon <? if ($important) echo 'tooltip-important'; ?>" data-tooltip <? if (!$html) printf('title="%s"', htmlReady($text)) ?> tabindex="0">
<? if ($html): ?>
<span class="tooltip-content"><?= $text ?></span>
<? endif; ?>
<span class="tooltip tooltip-icon <? if ($important) echo 'tooltip-important'; ?>"
tabindex="0" aria-label="<?= $html ? kill_format($text) : htmlReady($text) ?>">
<span class="tooltip-content"><?= $html ? $text : htmlReady($text) ?></span>
</span>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment