Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

feat(tooltip): adds md-direction so that users can specify tooltip dir... #1410

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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