From 3db79ab0ae6c66a3f4b932b5550586ba1ce4c93a Mon Sep 17 00:00:00 2001 From: Robert Messerle Date: Thu, 5 Feb 2015 16:54:51 -0800 Subject: [PATCH] feat(tooltip): adds `md-direction` so that users can specify tooltip direction Closes #1220. --- src/components/tooltip/tooltip.js | 123 ++++++++++++++----------- src/components/tooltip/tooltip.scss | 77 ++++------------ src/components/tooltip/tooltip.spec.js | 2 - 3 files changed, 86 insertions(+), 116 deletions(-) diff --git a/src/components/tooltip/tooltip.js b/src/components/tooltip/tooltip.js index b38e044ab05..dee87e4144f 100644 --- a/src/components/tooltip/tooltip.js +++ b/src/components/tooltip/tooltip.js @@ -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 { @@ -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') { @@ -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 @@ -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; @@ -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 }; + } } } diff --git a/src/components/tooltip/tooltip.scss b/src/components/tooltip/tooltip.scss index b0ae1021392..20feed19a0a 100644 --- a/src/components/tooltip/tooltip.scss +++ b/src/components/tooltip/tooltip.scss @@ -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; @@ -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 { @@ -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; - } - } } diff --git a/src/components/tooltip/tooltip.spec.js b/src/components/tooltip/tooltip.spec.js index f4777c0ca11..23c9846b151 100644 --- a/src/components/tooltip/tooltip.spec.js +++ b/src/components/tooltip/tooltip.spec.js @@ -18,10 +18,8 @@ describe(' 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);