Skip to content
This repository has been archived by the owner on Aug 29, 2023. It is now read-only.

Commit

Permalink
feat(tooltip): adds md-direction so that users can specify tooltip …
Browse files Browse the repository at this point in the history
…direction

Closes #1220.
Closes #1410.
  • Loading branch information
Robert Messerle authored and ThomasBurleson committed Feb 10, 2015
1 parent 33f677e commit 9c69c5c
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 116 deletions.
123 changes: 67 additions & 56 deletions src/components/tooltip/tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ angular.module('material.components.tooltip', [
* @param {expression=} md-visible Boolean bound to whether the tooltip is
* currently visible.
* @param {number=} md-delay How many milliseconds to wait to show the tooltip after the user focuses, hovers, or touches the parent. Defaults to 400ms.
* @param {string=} md-direction Which direction would you like the tooltip to go? Supports left, right, top, and bottom. Defaults to bottom.
*/
function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdTheming, $rootElement) {
function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdTheming, $rootElement, $animate, $q) {

var TOOLTIP_SHOW_DELAY = 400;
var TOOLTIP_SHOW_DELAY = 0;
var TOOLTIP_WINDOW_EDGE_SPACE = 8;

return {
Expand All @@ -55,6 +56,8 @@ function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdThe
function postLink(scope, element, attr, contentCtrl) {
$mdTheming(element);
var parent = element.parent();
var background = angular.element(element[0].getElementsByClassName('md-background')[0]);
var direction = attr.mdDirection;

// Keep looking for a higher parent if our current one has no pointer events
while ($window.getComputedStyle(parent[0])['pointer-events'] == 'none') {
Expand All @@ -78,24 +81,15 @@ function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdThe
element.attr('role', 'tooltip');
element.attr('id', attr.id || ('tooltip_' + $mdUtil.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[0].activeElement === parent[0]) return;
setVisible(false);
});
parent.on('focus mouseenter touchstart', function() { setVisible(true); });
parent.on('blur mouseleave touchend touchcancel', function() { if ($document[0].activeElement !== parent[0]) setVisible(false); });

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

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

// Be sure to completely cleanup the element on destroy
Expand All @@ -111,9 +105,8 @@ function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdThe

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

if (!setVisible.queued) {
if (value) {
setVisible.queued = true;
Expand All @@ -130,63 +123,81 @@ function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdThe

function showTooltip() {
// Insert the element before positioning it, so we can get position
// (tooltip is hidden by default)
element.removeClass('md-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.
// 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('md-show');
});

});
$animate.addClass(element, 'md-show');
$animate.addClass(background, 'md-show');
}

function hideTooltip() {
element.removeClass('md-show').addClass('md-hide');
parent.removeAttr('aria-describedby');
$timeout(function() {
if (scope.visible) return;
element.detach();
}, 200, false);
$q.all([
$animate.removeClass(background, 'md-show'),
$animate.removeClass(element, 'md-show')
]).then(function () {
if (!scope.visible) element.detach();
});
}

function positionTooltip() {
var tipRect = $mdUtil.elementRect(element, tooltipParent);
var parentRect = $mdUtil.elementRect(parent, tooltipParent);

// 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,
tooltipParent.prop('scrollWidth') - 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 > tooltipParent.prop('scrollHeight')) {
newPosition.top = parentRect.top - tipRect.height;
tipDirection = 'top';
var newPosition = getPosition(direction);

// If the user provided a direction, just nudge the tooltip onto the screen
// Otherwise, recalculate based on 'top' since default is 'bottom'
if (direction) {
newPosition = fitOnScreen(newPosition);
} else if (newPosition.top > tooltipParent.prop('scrollHeight') - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE) {
newPosition = fitOnScreen(getPosition('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('md-direction', tipDirection);

positionBackground();

function positionBackground () {
var size = direction === 'left' || direction === 'right'
? Math.sqrt(Math.pow(tipRect.width, 2) + Math.pow(tipRect.height / 2, 2)) * 2
: Math.sqrt(Math.pow(tipRect.width / 2, 2) + Math.pow(tipRect.height, 2)) * 2,
position = direction === 'left' ? { left: 100, top: 50 }
: direction === 'right' ? { left: 0, top: 50 }
: direction === 'top' ? { left: 50, top: 100 }
: { left: 50, top: 0 };
background.css({
width: size + 'px',
height: size + 'px',
left: position.left + '%',
top: position.top + '%'
});
}

function fitOnScreen (pos) {
var newPosition = {};
newPosition.left = Math.min( pos.left, tooltipParent.prop('scrollWidth') - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE );
newPosition.left = Math.max( pos.left, TOOLTIP_WINDOW_EDGE_SPACE );
newPosition.top = Math.min( pos.top, tooltipParent.prop('scrollHeight') - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE );
newPosition.top = Math.max( pos.top, TOOLTIP_WINDOW_EDGE_SPACE );
return newPosition;
}

function getPosition (dir) {
return dir === 'left'
? { left: parentRect.left - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE,
top: parentRect.top + parentRect.height / 2 - tipRect.height / 2 }
: dir === 'right'
? { left: parentRect.left + parentRect.width + TOOLTIP_WINDOW_EDGE_SPACE,
top: parentRect.top + parentRect.height / 2 - tipRect.height / 2 }
: dir === 'top'
? { left: parentRect.left + parentRect.width / 2 - tipRect.width / 2,
top: parentRect.top - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE }
: { left: parentRect.left + parentRect.width / 2 - tipRect.width / 2,
top: parentRect.top + parentRect.height + TOOLTIP_WINDOW_EDGE_SPACE };
}
}

}
Expand Down
77 changes: 19 additions & 58 deletions src/components/tooltip/tooltip.scss
Original file line number Diff line number Diff line change
@@ -1,21 +1,3 @@
@keyframes tooltipBackgroundShow {
0% {
transform: scale(0.2);
opacity: 0.25;
}
50% {
opacity: 1;
}
100% {
transform: scale(1.0);
opacity: 1;
}
}
@keyframes tooltipBackgroundHide {
0% { opacity: 1; }
100% { opacity: 0; }
}

md-tooltip {
position: absolute;
font-size: 14px;
Expand All @@ -24,26 +6,27 @@ md-tooltip {
pointer-events: none;
border-radius: 4px;

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

.md-background {
position: absolute;
left: 50%;
width: 256px;
height: 256px;
margin-left: -128px;
margin-top: -128px;
border-radius: 256px;

opacity: 0.25;
transform: scale(0.2);
border-radius: 50%;
transform: translate(-50%, -50%) scale(0);
opacity: 1;
&.md-show-add {
transition: $swift-ease-out;
transform: translate(-50%, -50%) scale(0);
opacity: 0;
}
&.md-show, &.md-show-add-active {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}
&.md-show-remove {
transition: $swift-ease-in;
&.md-show-remove-active {
transform: translate(-50%, -50%) scale(0);
opacity: 0;
}
}
}

.md-content {
Expand All @@ -67,30 +50,8 @@ md-tooltip {
pointer-events: auto;
transform: translate3d(0,0,0);

.md-background {
transform: scale(1.0);
opacity: 1.0;
animation: tooltipBackgroundShow linear;
}
.md-content {
opacity: 0.99;
}
}
&.md-hide .md-background {
transform: scale(1.0);
opacity: 0;
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}"].md-show .md-background {
$duration: 1000 - $i * 100;
animation-duration: #{$duration}ms;
}
}
}
2 changes: 0 additions & 2 deletions src/components/tooltip/tooltip.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ describe('<md-tooltip> directive', function() {
$rootScope.$apply('isVisible = true');
expect(findTooltip().length).toBe(1);
expect(findTooltip().hasClass('md-show')).toBe(true);
expect(findTooltip().hasClass('md-hide')).toBe(false);

$rootScope.$apply('isVisible = false');
expect(findTooltip().hasClass('md-hide')).toBe(true);
expect(findTooltip().hasClass('md-show')).toBe(false);
$timeout.flush();
expect(findTooltip().length).toBe(0);
Expand Down

0 comments on commit 9c69c5c

Please sign in to comment.