Skip to content

Commit

Permalink
feat(tooltip): add tooltip component
Browse files Browse the repository at this point in the history
Closes angular#354. Closes angular#81.
  • Loading branch information
ajoslin committed Oct 2, 2014
1 parent c7ef10c commit 9f9b089
Show file tree
Hide file tree
Showing 11 changed files with 451 additions and 1 deletion.
1 change: 1 addition & 0 deletions config/build.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ module.exports = {
'src/components/tabs/js/*.js',
'src/components/toast/toast.js',
'src/components/toolbar/toolbar.js',
'src/components/tooltip/tooltip.js',
'src/components/whiteframe/whiteframe.js',
'src/components/divider/divider.js',
'src/components/linearProgress/linearProgress.js',
Expand Down
6 changes: 5 additions & 1 deletion src/components/content/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,13 @@ angular.module('material.components.content', [
function materialContentDirective() {
return {
restrict: 'E',
controller: angular.noop,
controller: ['$element', ContentController],
link: function($scope, $element, $attr) {
$scope.$broadcast('$materialContentLoaded', $element);
}
};

function ContentController($element) {
this.$element = $element;
}
}
1 change: 1 addition & 0 deletions src/components/tooltip/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Create a tooltip.
101 changes: 101 additions & 0 deletions src/components/tooltip/_tooltip.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
@include keyframes(tooltipBackgroundShow) {
0% {
@include transform(scale(0.2));
opacity: 0.25;
}
50% {
opacity: 1;
}
100% {
@include transform(scale(1.0));
opacity: 1;
}
}
@include keyframes(tooltipBackgroundHide) {
0% { opacity: 1; }
100% { opacity: 0; }
}

material-tooltip {
position: absolute;
font-size: 14px;
z-index: $z-index-tooltip;
overflow: hidden;
pointer-events: none;
color: white;
border-radius: 4px;

&[tooltip-direction="bottom"] {
@include transform(translate3d(0, -30%, 0));
margin-top: 8px;
}
&[tooltip-direction="top"] {
@include transform(translate3d(0, 30%, 0));
margin-bottom: 8px;
}

.tooltip-background {
background: rgb(115,115,115);
position: absolute;
left: 50%;
width: 256px;
height: 256px;
margin-left: -128px;
margin-top: -128px;
border-radius: 256px;

opacity: 0.25;
@include transform(scale(0.2));
}

.tooltip-content {
max-width: 240px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

padding: 8px;
background: transparent;
opacity: 0.3;
@include transition(inherit);
}

&.tooltip-show,
&.tooltip-hide {
@include transition(0.2s ease-out);
transition-property: transform, opacity;
-webkit-transition-property: -webkit-transform, opacity;
}

&.tooltip-show {
pointer-events: auto;
@include transform(translate3d(0,0,0));

.tooltip-background {
@include transform(scale(1.0));
opacity: 1.0;
@include animation(tooltipBackgroundShow linear);
}
.tooltip-content {
opacity: 0.99;
}
}
&.tooltip-hide .tooltip-background {
@include transform(scale(1.0));
opacity: 0;
@include animation(tooltipBackgroundHide 0.2s linear);
}

/**
* Depending on the tooltip's size as a multiple of 32 (set by JS),
* change the background's animation duration.
* The larger the tooltip, the less time the background should take to ripple outwards.
*/
@for $i from 1 through 8 {
&[width-32="#{$i}"].tooltip-show .tooltip-background {
$duration: 1000 - $i * 100;
animation-duration: #{$duration}ms;
-webkit-animation-duration: #{$duration}ms;
}
}
}
32 changes: 32 additions & 0 deletions src/components/tooltip/demo1/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<div ng-controller="AppCtrl">
<material-content class="material-content-padding">

<material-button class="material-button-fab material-button-fab-top-left">
<material-tooltip visible="showTooltip">
Refresh
</material-tooltip>
<material-icon icon="/img/icons/ic_refresh_24px.svg">
</material-icon>
</material-button>

<br/>
<br/>
<br/>
<br/>
<br/>

<p>
<div>
The tooltip is visible when the button is hovered, focused, or touched.
</div>
<div>
Additionally, the tooltip's visibility is bound to the checkbox below.
</div>
</p>

<material-checkbox ng-model="showTooltip">
Tooltip is shown
</material-checkbox>

</material-content>
</div>
4 changes: 4 additions & 0 deletions src/components/tooltip/demo1/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
angular.module('tooltipDemo1', ['ngMaterial'])

.controller('AppCtrl', function($scope) {
});
10 changes: 10 additions & 0 deletions src/components/tooltip/module.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"module": "material.components.tooltip",
"name": "Tooltip",
"demos": {
"demo1": {
"name": "Tooltip Basic Usage",
"files": ["demo1/*"]
}
}
}
187 changes: 187 additions & 0 deletions src/components/tooltip/tooltip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/**
* @ngdoc module
* @name material.components.tooltip
*/
angular.module('material.components.tooltip', [])

.directive('materialTooltip', [
'$timeout',
'$window',
'$$rAF',
'$document',
MaterialTooltipDirective
]);

/**
* @ngdoc directive
* @name materialTooltip
* @module material.components.tooltip
* @description
* Tooltips are used to describe elements that are interactive and primarily graphical (not textual).
*
* Place a `<material-tooltip>` as a child of the element it describes.
*
* A tooltip will activate when the user focuses, hovers over, or touches the parent.
*
* @usage
* <hljs lang="html">
* <material-icon icon="/img/icons/ic_play_arrow_24px.svg">
* <material-tooltip>
* Play Music
* </material-tooltip>
* </material-icon>
* </hljs>
*
* @param {expression=} visible Boolean bound to whether the tooltip is
* currently visible.
*/
function MaterialTooltipDirective($timeout, $window, $$rAF, $document) {

var TOOLTIP_SHOW_DELAY = 400;
var TOOLTIP_WINDOW_EDGE_SPACE = 8;
// We have to append tooltips to the body, because we use
// getBoundingClientRect().
// to find where to append the tooltip.
var tooltipParent = angular.element(document.body);

return {
restrict: 'E',
transclude: true,
require: '^?materialContent',
template:
'<div class="tooltip-background"></div>' +
'<div class="tooltip-content" ng-transclude></div>',
scope: {
visible: '=?'
},
link: postLink
};

function postLink(scope, element, attr, contentCtrl) {
var parent = element.parent();

// We will re-attach tooltip when visible
element.detach();
element.attr('role', 'tooltip');
element.attr('id', attr.id || Util.nextUid());

parent.on('focus mouseenter touchstart', function() {
setVisible(true);
});
parent.on('blur mouseleave touchend touchcancel', function() {
// Don't hide the tooltip if the parent is still focused.
if (document.activeElement === parent[0]) return;
setVisible(false);
});

scope.$watch('visible', function(isVisible) {
if (isVisible) showTooltip();
else hideTooltip();
});

var debouncedOnResize = $$rAF.debounce(onWindowResize);
angular.element($window).on('resize', debouncedOnResize);
function onWindowResize() {
// Reposition on resize
if (scope.visible) positionTooltip();
}

// Be sure to completely cleanup the element on destroy
scope.$on('$destroy', function() {
scope.visible = false;
element.remove();
angular.element($window).off('resize', debouncedOnResize);
});

// *******
// Methods
// *******

// If setting visible to true, debounce to TOOLTIP_SHOW_DELAY ms
// If setting visible to false and no timeout is active, instantly hide the tooltip.
function setVisible(value) {
setVisible.value = !!value;

if (!setVisible.queued) {
if (value) {
setVisible.queued = true;
$timeout(function() {
scope.visible = setVisible.value;
setVisible.queued = false;
}, TOOLTIP_SHOW_DELAY);

} else {
$timeout(function() { scope.visible = false; });
}
}
}

function showTooltip() {
// Insert the element before positioning it, so we can get position
// (tooltip is hidden by default)
element.removeClass('tooltip-hide');
parent.attr('aria-describedby', element.attr('id'));
tooltipParent.append(element);

// Wait until the element has been in the dom for two frames before
// fading it in.
// Additionally, we position the tooltip twice to avoid positioning bugs
//positionTooltip();
$$rAF(function() {

$$rAF(function() {
positionTooltip();
if (!scope.visible) return;
element.addClass('tooltip-show');
});

});
}

function hideTooltip() {
element.removeClass('tooltip-show').addClass('tooltip-hide');
parent.removeAttr('aria-describedby');
$timeout(function() {
if (scope.visible) return;
element.detach();
}, 200, false);
}

function positionTooltip(rerun) {
var tipRect = element[0].getBoundingClientRect();
var parentRect = parent[0].getBoundingClientRect();

if (contentCtrl) {
parentRect.top += contentCtrl.$element.prop('scrollTop');
parentRect.left += contentCtrl.$element.prop('scrollLeft');
}

// Default to bottom position if possible
var tipDirection = 'bottom';
var newPosition = {
left: parentRect.left + parentRect.width / 2 - tipRect.width / 2,
top: parentRect.top + parentRect.height
};

// If element bleeds over left/right of the window, place it on the edge of the window.
newPosition.left = Math.min(
newPosition.left,
$window.innerWidth - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE
);
newPosition.left = Math.max(newPosition.left, TOOLTIP_WINDOW_EDGE_SPACE);

// If element bleeds over the bottom of the window, place it above the parent.
if (newPosition.top + tipRect.height > $window.innerHeight) {
newPosition.top = parentRect.top - tipRect.height;
tipDirection = 'top';
}

element.css({top: newPosition.top + 'px', left: newPosition.left + 'px'});
// Tell the CSS the size of this tooltip, as a multiple of 32.
element.attr('width-32', Math.ceil(tipRect.width / 32));
element.attr('tooltip-direction', tipDirection);
}

}

}
Loading

0 comments on commit 9f9b089

Please sign in to comment.