diff --git a/src/components/fabSpeedDial/fabController.js b/src/components/fabSpeedDial/fabController.js index 226b764a81b..01ab1b88ae9 100644 --- a/src/components/fabSpeedDial/fabController.js +++ b/src/components/fabSpeedDial/fabController.js @@ -44,11 +44,7 @@ function setupListeners() { var eventTypes = [ - '$md.pressdown', - - 'click', // Fired via keyboard ENTER - - 'focusin', 'focusout' + 'click', 'focusin', 'focusout' ]; // Add our listeners @@ -68,34 +64,25 @@ }); } - var recentEvent; + var closeTimeout; function parseEvents(event) { - // If we've had a recent press/click event, or material is sending us an additional event, - // ignore it - if (recentEvent && (isClick(recentEvent) || recentEvent.$material)) { - return; - } - - // Otherwise, handle our events - if (isClick(event)) { + // If the event is a click, just handle it + if (event.type == 'click') { handleItemClick(event); - - // Store our recent click event - recentEvent = event; - } else if (event.type == 'focusin') { - vm.open(); - } else if (event.type == 'focusout') { - vm.close(); } - // Clear the recent event after all others have fired so we stop ignoring - $timeout(function() { - recentEvent = null; - }, 100, false); - } + // If we focusout, set a timeout to close the element + if (event.type == 'focusout' && !closeTimeout) { + closeTimeout = $timeout(function() { + vm.close(); + }, 100, false); + } - function isClick(event) { - return event.type == '$md.pressdown' || event.type == 'click'; + // If we see a focusin and there is a timeout about to run, cancel it so we stay open + if (event.type == 'focusin' && closeTimeout) { + $timeout.cancel(closeTimeout); + closeTimeout = null; + } } function resetActionIndex() { @@ -154,19 +141,34 @@ } function enableKeyboard() { - angular.element(document).on('keydown', keyPressed); + $element.on('keydown', keyPressed); + angular.element(document).on('click', checkForOutsideClick); + angular.element(document).on('touchend', checkForOutsideClick); - // TODO: On desktop, we should be able to reset the indexes so you cannot tab through + // TODO: On desktop, we should be able to reset the indexes so you cannot tab through, but + // this breaks accessibility, especially on mobile, since you have no arrow keys to press //resetActionTabIndexes(); } function disableKeyboard() { - angular.element(document).off('keydown', keyPressed); + $element.off('keydown', keyPressed); + angular.element(document).off('click', checkForOutsideClick); + angular.element(document).off('touchend', checkForOutsideClick); + } + + function checkForOutsideClick(event) { + if (event.target) { + var closestTrigger = $mdUtil.getClosest(event.target, 'md-fab-trigger'); + var closestActions = $mdUtil.getClosest(event.target, 'md-fab-actions'); + + if (!closestTrigger && !closestActions) { + vm.close(); + } + } } function keyPressed(event) { switch (event.which) { - case $mdConstant.KEY_CODE.SPACE: event.preventDefault(); return false; case $mdConstant.KEY_CODE.ESCAPE: vm.close(); event.preventDefault(); return false; case $mdConstant.KEY_CODE.LEFT_ARROW: doKeyLeft(event); return false; case $mdConstant.KEY_CODE.UP_ARROW: doKeyUp(event); return false; diff --git a/src/components/fabSpeedDial/fabSpeedDial.js b/src/components/fabSpeedDial/fabSpeedDial.js index b274c70e185..bd9c29b23cd 100644 --- a/src/components/fabSpeedDial/fabSpeedDial.js +++ b/src/components/fabSpeedDial/fabSpeedDial.js @@ -1,6 +1,13 @@ (function() { 'use strict'; + /** + * The duration of the CSS animation in milliseconds. + * + * @type {number} + */ + var cssAnimationDuration = 300; + /** * @ngdoc module * @name material.components.fabSpeedDial @@ -103,7 +110,9 @@ } } - function MdFabSpeedDialFlingAnimation() { + function MdFabSpeedDialFlingAnimation($timeout) { + function delayDone(done) { $timeout(done, cssAnimationDuration, false); } + function runAnimation(element) { var el = element[0]; var ctrl = element.controller('mdFabSpeedDial'); @@ -169,17 +178,19 @@ addClass: function(element, className, done) { if (element.hasClass('md-fling')) { runAnimation(element); - done(); } + delayDone(done); }, removeClass: function(element, className, done) { runAnimation(element); - done(); + delayDone(done); } } } - function MdFabSpeedDialScaleAnimation() { + function MdFabSpeedDialScaleAnimation($timeout) { + function delayDone(done) { $timeout(done, cssAnimationDuration, false); } + var delay = 65; function runAnimation(element) { @@ -210,12 +221,12 @@ return { addClass: function(element, className, done) { runAnimation(element); - done(); + delayDone(done); }, removeClass: function(element, className, done) { runAnimation(element); - done(); + delayDone(done); } } } diff --git a/src/components/fabSpeedDial/fabSpeedDial.scss b/src/components/fabSpeedDial/fabSpeedDial.scss index 9745a7376a9..7ccf927927d 100644 --- a/src/components/fabSpeedDial/fabSpeedDial.scss +++ b/src/components/fabSpeedDial/fabSpeedDial.scss @@ -31,7 +31,6 @@ md-fab-speed-dial { &.md-is-open { .md-fab-action-item { - visibility: visible; align-items: center; } } @@ -43,7 +42,6 @@ md-fab-speed-dial { height: auto; .md-fab-action-item { - visibility: hidden; transition: $swift-ease-in; } } @@ -108,6 +106,15 @@ md-fab-speed-dial { } } + /* + * Hide some graphics glitches if switching animation types + */ + &.md-fling-remove, &.md-scale-remove { + .md-fab-action-item > * { + visibility: hidden; + } + } + /* * Handle the animations */ diff --git a/src/components/fabSpeedDial/fabSpeedDial.spec.js b/src/components/fabSpeedDial/fabSpeedDial.spec.js index cbc496bc537..3182437f177 100644 --- a/src/components/fabSpeedDial/fabSpeedDial.spec.js +++ b/src/components/fabSpeedDial/fabSpeedDial.spec.js @@ -51,7 +51,7 @@ describe(' directive', function() { it('toggles the menu when the trigger clicked', inject(function() { build( - '' + + '' + ' ' + ' ' + ' ' + @@ -107,55 +107,7 @@ describe(' directive', function() { expect(controller.isOpen).toBe(false); })); - it('opens the menu when the trigger is focused', inject(function() { - build( - '' + - ' ' + - ' ' + - ' ' + - '' - ); - - var focusEvent = { - type: 'focusin', - target: element.find('md-fab-trigger').find('md-button') - }; - - element.triggerHandler(focusEvent); - pageScope.$digest(); - expect(controller.isOpen).toBe(true); - })); - - it('closes the menu when the trigger is blurred', inject(function() { - build( - '' + - ' ' + - ' ' + - ' ' + - '' - ); - - var focusInEvent = { - type: 'focusin', - target: element.find('md-fab-trigger').find('md-button') - }; - - var focusOutEvent = { - type: 'focusout', - target: element.find('md-fab-trigger').find('md-button') - }; - - element.triggerHandler(focusInEvent); - pageScope.$digest(); - expect(controller.isOpen).toBe(true); - - element.triggerHandler(focusOutEvent); - pageScope.$digest(); - expect(controller.isOpen).toBe(false); - })); - - - it('properly finishes the fling animation', inject(function(mdFabSpeedDialFlingAnimation) { + it('properly finishes the fling animation', inject(function(mdFabSpeedDialFlingAnimation, $timeout) { build( '' + ' ' + @@ -167,9 +119,11 @@ describe(' directive', function() { var removeDone = jasmine.createSpy('removeDone'); mdFabSpeedDialFlingAnimation.addClass(element, 'md-is-open', addDone); + $timeout.flush(); expect(addDone).toHaveBeenCalled(); mdFabSpeedDialFlingAnimation.removeClass(element, 'md-is-open', removeDone); + $timeout.flush(); expect(removeDone).toHaveBeenCalled(); })); @@ -185,9 +139,11 @@ describe(' directive', function() { var removeDone = jasmine.createSpy('removeDone'); mdFabSpeedDialScaleAnimation.addClass(element, 'md-is-open', addDone); + $timeout.flush(); expect(addDone).toHaveBeenCalled(); mdFabSpeedDialScaleAnimation.removeClass(element, 'md-is-open', removeDone); + $timeout.flush(); expect(removeDone).toHaveBeenCalled(); }));