Commit 67e4b7a2 authored by Robert Knight's avatar Robert Knight Committed by Nick Stenning

Add custom tooltips for annotation card footer actions (#3299)

* Add an 'h-tooltip' attribute directive which displays a tooltip above
   the associated element as soon as it is hovered and removes the
   tooltip on mouseout or when the associated element is destroyed.

 * Use the custom tooltip for buttons in the annotation card footer
   and remove the labels.
parent fb802a44
......@@ -163,6 +163,7 @@ module.exports = angular.module('h', [
.directive('formValidate', require('./directive/form-validate'))
.directive('groupList', require('./directive/group-list').directive)
.directive('hAutofocus', require('./directive/h-autofocus'))
.directive('hTooltip', require('./directive/h-tooltip'))
.directive('loggedoutMessage', require('./directive/loggedout-message'))
.directive('loginForm', require('./directive/login-form').directive)
.directive('markdown', require('./directive/markdown'))
......
'use strict';
var theTooltip;
/**
* A custom tooltip similar to the one used in Google Docs which appears
* instantly when activated on a target element.
*
* The tooltip is displayed and hidden by setting its target element.
*
* var tooltip = new Tooltip(document.body);
* tooltip.setState({target: aWidget}); // Show tooltip
* tooltip.setState({target: null}); // Hide tooltip
*
* The tooltip's label is derived from the target element's 'aria-label'
* attribute.
*
* @param {Element} rootElement - The container for the tooltip.
*/
function Tooltip(rootElement) {
this.setState = function (state) {
this.state = Object.freeze(Object.assign({}, this.state, state));
this.render();
};
this.render = function () {
var TOOLTIP_ARROW_HEIGHT = 7;
if (!this.state.target) {
this._el.style.visibility = 'hidden';
return;
}
var target = this.state.target;
var label = target.getAttribute('aria-label');
this._labelEl.textContent = label;
var tooltipRect = this._el.getBoundingClientRect();
var targetRect = target.getBoundingClientRect();
var top = targetRect.top - tooltipRect.height - TOOLTIP_ARROW_HEIGHT;
var left = targetRect.right - tooltipRect.width;
Object.assign(this._el.style, {
visibility: '',
top: top + 'px',
left: left + 'px',
});
};
this._el = rootElement.ownerDocument.createElement('div');
this._el.innerHTML = '<span class="tooltip-label js-tooltip-label"></span>';
this._el.className = 'tooltip';
rootElement.appendChild(this._el);
this._labelEl = this._el.querySelector('.js-tooltip-label');
this.setState({});
}
/**
* Attribute directive which displays a custom tooltip when hovering the
* associated element.
*
* The associated element should use the `aria-label` attribute to specify
* the tooltip instead of the `title` attribute, which would trigger the
* display of the browser's native tooltip.
*
* Example: '<button aria-label="Tooltip label" h-tooltip></button>'
*/
module.exports = function () {
if (!theTooltip) {
theTooltip = new Tooltip(document.body);
}
return {
restrict: 'A',
link: function ($scope, $element) {
var el = $element[0];
el.addEventListener('mouseover', function () {
theTooltip.setState({target: el});
});
el.addEventListener('mouseout', function () {
theTooltip.setState({target: null});
});
// Hide the tooltip if the element is removed whilst the tooltip is active
$scope.$on('$destroy', function () {
if (theTooltip.state.target === el) {
theTooltip.setState({target: null});
}
});
},
};
};
'use strict';
var angular = require('angular');
var util = require('./util');
function testComponent() {
return {
restrict: 'E',
template: '<div aria-label="Share" h-tooltip>Label</div>',
};
}
describe('h-tooltip', function () {
var targetEl;
var tooltipEl;
before(function () {
angular.module('app', [])
.directive('hTooltip', require('../h-tooltip'))
.directive('test', testComponent);
});
beforeEach(function () {
angular.mock.module('app');
var testEl = util.createDirective(document, 'test', {});
targetEl = testEl[0].querySelector('div');
tooltipEl = document.querySelector('.tooltip');
});
afterEach(function () {
var testEl = document.querySelector('test');
testEl.parentNode.removeChild(testEl);
});
it('appears when the target is hovered', function () {
util.sendEvent(targetEl, 'mouseover');
assert.equal(tooltipEl.style.visibility, '');
});
it('sets the label from the target\'s "aria-label" attribute', function () {
util.sendEvent(targetEl, 'mouseover');
assert.equal(tooltipEl.textContent, 'Share');
});
it('disappears when the target is unhovered', function () {
util.sendEvent(targetEl, 'mouseout');
assert.equal(tooltipEl.style.visibility, 'hidden');
});
it('disappears when the target is destroyed', function () {
util.sendEvent(targetEl, 'mouseover');
angular.element(targetEl).scope().$broadcast('$destroy');
assert.equal(tooltipEl.style.visibility, 'hidden');
});
});
......@@ -43,7 +43,7 @@ function ngModule(inject, name) {
* var domElement = createDirective(document, 'myComponent', {
* attrA: 'initial-value'
* }, {
* scopePropery: scopeValue
* scopeProperty: scopeValue
* },
* 'Hello, world!');
*
......
......@@ -27,6 +27,7 @@ $base-line-height: 20px;
@import './spinner';
@import './tags-input';
@import './thread';
@import './tooltip';
@import './top-bar';
// Top-level styles
......
@mixin tooltip-arrow($rotation) {
transform: rotate($rotation);
background: $grey-7;
border-bottom: 1px solid rgba(0,0,0,0.20);
border-right: 1px solid rgba(0,0,0,0.20);
content: "";
display: block;
height: 6px;
left: 0;
margin-left: auto;
margin-right: 5px;
position: absolute;
right: 0;
width: 6px;
}
.tooltip {
@include font-small;
border-radius: 2px;
position: fixed;
background-color: $grey-7;
color: white;
font-weight: bold;
padding-left: 5px;
padding-right: 5px;
padding-top: 4px;
padding-bottom: 4px;
z-index: $zindex-tooltip;
}
// Arrow at the bottom of the tooltip pointing down at the target element.
.tooltip:before {
@include tooltip-arrow(45deg);
content: "";
top: calc(100% - 5px);
}
.tooltip-label {
// Make the label a positioned element so that it appears _above_ the
// tooltip's arrow, which partially overlaps the content of the tooltip.
position: relative;
}
......@@ -158,6 +158,7 @@ $anim-duration-normal: .3s; // a good default choice for transition lengths
// Z-Index Scale
// -------------------------
$zindex-dropdown-menu: 10;
$zindex-tooltip: 20;
// Other Variables
......
......@@ -118,7 +118,8 @@
<div class="annotation-form-actions" ng-if="vm.editing()" ng-switch="vm.action">
<button ng-switch-when="delete"
ng-click="vm.save()"
class="dropdown-menu-btn"><i class="h-icon-check btn-icon"></i> Delete</button>
class="dropdown-menu-btn"
aria-label="Delete" h-tooltip><i class="h-icon-check btn-icon"></i></button>
<publish-annotation-btn
class="publish-annotation-btn"
group="vm.group()"
......@@ -155,34 +156,30 @@
<div ng-show="vm.isSaving">Saving…</div>
<button class="btn btn-clean annotation-action-btn"
ng-show="vm.authorize('update') && !vm.isSaving"
ng-click="vm.edit()">
ng-click="vm.edit()"
aria-label="Edit"
h-tooltip>
<i class="h-icon-annotation-edit btn-icon "></i>
<span class="annotation-action-btn__label">
Edit
</span>
</button>
<button class="btn btn-clean annotation-action-btn"
ng-show="vm.authorize('delete')"
ng-click="vm.delete()">
ng-click="vm.delete()"
aria-label="Delete"
h-tooltip>
<i class="h-icon-annotation-delete btn-icon "></i>
<span class="annotation-action-btn__label">
Delete
</span>
</button>
<button class="btn btn-clean annotation-action-btn"
ng-click="vm.reply()">
ng-click="vm.reply()"
aria-label="Reply"
h-tooltip>
<i class="h-icon-annotation-reply btn-icon "></i>
<span class="annotation-action-btn__label">
Reply
</span>
</button>
<span class="share-dialog-wrapper" ng-if="!vm.feature('direct_linking')">
<button class="btn btn-clean annotation-action-btn"
ng-click="vm.showShareDialog = !vm.showShareDialog">
ng-click="vm.showShareDialog = !vm.showShareDialog"
aria-label="Link"
h-tooltip>
<i class="h-icon-link btn-icon "></i>
<span class="annotation-action-btn__label">
Link
</span>
</button>
<span class="share-dialog share-dialog--actions" ng-click="$event.stopPropagation()">
<a target="_blank"
......@@ -195,11 +192,10 @@
</span>
<span class="share-dialog-wrapper" ng-if="vm.feature('direct_linking')">
<button class="btn btn-clean annotation-action-btn"
ng-click="vm.showShareDialog = true">
ng-click="vm.showShareDialog = true"
aria-label="Share"
h-tooltip>
<i class="h-icon-annotation-share btn-icon "></i>
<span class="annotation-action-btn__label">
Share
</span>
</button>
<annotation-share-dialog
group="vm.group()"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment