From 3b051ac77949d8c7afae420f82aee7fcc2d791d1 Mon Sep 17 00:00:00 2001 From: Wesley Cho Date: Sat, 5 Sep 2015 03:34:40 -0700 Subject: [PATCH] feat(tooltip): hide tooltip when `esc` is hit - Hide tooltip when `esc` is hit for accessibility Closes #4367 Resolves #4248 --- src/modal/modal.js | 57 +------------------ src/stackedMap/stackedMap.js | 54 ++++++++++++++++++ .../test/stackedMap.spec.js | 2 +- src/tooltip/test/tooltip.spec.js | 41 ++++++++++++- src/tooltip/tooltip.js | 25 +++++++- 5 files changed, 118 insertions(+), 61 deletions(-) create mode 100644 src/stackedMap/stackedMap.js rename src/{modal => stackedMap}/test/stackedMap.spec.js (99%) diff --git a/src/modal/modal.js b/src/modal/modal.js index bd284c0a02..85d159dafd 100644 --- a/src/modal/modal.js +++ b/src/modal/modal.js @@ -1,59 +1,4 @@ -angular.module('ui.bootstrap.modal', []) - -/** - * A helper, internal data structure that acts as a map but also allows getting / removing - * elements in the LIFO order - */ - .factory('$$stackedMap', function() { - return { - createNew: function() { - var stack = []; - - return { - add: function(key, value) { - stack.push({ - key: key, - value: value - }); - }, - get: function(key) { - for (var i = 0; i < stack.length; i++) { - if (key == stack[i].key) { - return stack[i]; - } - } - }, - keys: function() { - var keys = []; - for (var i = 0; i < stack.length; i++) { - keys.push(stack[i].key); - } - return keys; - }, - top: function() { - return stack[stack.length - 1]; - }, - remove: function(key) { - var idx = -1; - for (var i = 0; i < stack.length; i++) { - if (key == stack[i].key) { - idx = i; - break; - } - } - return stack.splice(idx, 1)[0]; - }, - removeTop: function() { - return stack.splice(stack.length - 1, 1)[0]; - }, - length: function() { - return stack.length; - } - }; - } - }; - }) - +angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap']) /** * A helper, internal data structure that stores all references attached to key */ diff --git a/src/stackedMap/stackedMap.js b/src/stackedMap/stackedMap.js new file mode 100644 index 0000000000..bd97fb60db --- /dev/null +++ b/src/stackedMap/stackedMap.js @@ -0,0 +1,54 @@ +angular.module('ui.bootstrap.stackedMap', []) +/** + * A helper, internal data structure that acts as a map but also allows getting / removing + * elements in the LIFO order + */ + .factory('$$stackedMap', function() { + return { + createNew: function() { + var stack = []; + + return { + add: function(key, value) { + stack.push({ + key: key, + value: value + }); + }, + get: function(key) { + for (var i = 0; i < stack.length; i++) { + if (key == stack[i].key) { + return stack[i]; + } + } + }, + keys: function() { + var keys = []; + for (var i = 0; i < stack.length; i++) { + keys.push(stack[i].key); + } + return keys; + }, + top: function() { + return stack[stack.length - 1]; + }, + remove: function(key) { + var idx = -1; + for (var i = 0; i < stack.length; i++) { + if (key == stack[i].key) { + idx = i; + break; + } + } + return stack.splice(idx, 1)[0]; + }, + removeTop: function() { + return stack.splice(stack.length - 1, 1)[0]; + }, + length: function() { + return stack.length; + } + }; + } + }; + }); \ No newline at end of file diff --git a/src/modal/test/stackedMap.spec.js b/src/stackedMap/test/stackedMap.spec.js similarity index 99% rename from src/modal/test/stackedMap.spec.js rename to src/stackedMap/test/stackedMap.spec.js index 2e422eedf2..62ad5d698e 100644 --- a/src/modal/test/stackedMap.spec.js +++ b/src/stackedMap/test/stackedMap.spec.js @@ -49,4 +49,4 @@ describe('stacked map', function() { it('should ignore removal of non-existing elements', function() { expect(stackedMap.remove('non-existing')).toBeUndefined(); }); -}); \ No newline at end of file +}); diff --git a/src/tooltip/test/tooltip.spec.js b/src/tooltip/test/tooltip.spec.js index a5e23d7f12..a86bd2017c 100644 --- a/src/tooltip/test/tooltip.spec.js +++ b/src/tooltip/test/tooltip.spec.js @@ -3,7 +3,8 @@ describe('tooltip', function() { elmBody, scope, elmScope, - tooltipScope; + tooltipScope, + $document; // load the tooltip code beforeEach(module('ui.bootstrap.tooltip')); @@ -11,11 +12,12 @@ describe('tooltip', function() { // load the template beforeEach(module('template/tooltip/tooltip-popup.html')); - beforeEach(inject(function($rootScope, $compile) { + beforeEach(inject(function($rootScope, $compile, _$document_) { elmBody = angular.element( '
Selector Text
' ); + $document = _$document_; scope = $rootScope; $compile(elmBody)(scope); scope.$digest(); @@ -319,6 +321,41 @@ describe('tooltip', function() { expect(tooltipScope.isOpen).toBe(true); }); + + it('should close the tooltips in order', inject(function($compile) { + var elm2 = $compile('
Selector Text
')(scope); + scope.$digest(); + elm2 = elm2.find('span'); + var tooltipScope2 = elm2.scope().$$childTail; + tooltipScope2.isOpen = false; + scope.$digest(); + + trigger(elm, 'mouseenter'); + $timeout.flush(); + expect(tooltipScope.isOpen).toBe(true); + expect(tooltipScope2.isOpen).toBe(false); + + trigger(elm2, 'mouseenter'); + $timeout.flush(); + expect(tooltipScope.isOpen).toBe(true); + expect(tooltipScope2.isOpen).toBe(true); + + var evt = $.Event('keypress'); + evt.which = 27; + + $document.trigger(evt); + + expect(tooltipScope.isOpen).toBe(true); + expect(tooltipScope2.isOpen).toBe(false); + + var evt2 = $.Event('keypress'); + evt2.which = 27; + + $document.trigger(evt2); + + expect(tooltipScope.isOpen).toBe(false); + expect(tooltipScope2.isOpen).toBe(false); + })); }); describe('with an is-open attribute', function() { diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index 2e10b221ca..c88dc0eb4d 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -3,7 +3,7 @@ * function, placement as a function, inside, support for more triggers than * just mouse enter/leave, html tooltips, and selector delegation. */ -angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position']) +angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.stackedMap']) /** * The $tooltip service creates tooltip- and popover-like directives as well as @@ -66,7 +66,19 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position']) * Returns the actual instance of the $tooltip service. * TODO support multiple triggers */ - this.$get = ['$window', '$compile', '$timeout', '$document', '$position', '$interpolate', '$rootScope', '$parse', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse) { + this.$get = ['$window', '$compile', '$timeout', '$document', '$position', '$interpolate', '$rootScope', '$parse', '$$stackedMap', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse, $$stackedMap) { + var openedTooltips = $$stackedMap.createNew(); + $document.on('keypress', function(e) { + if (e.which === 27) { + var last = openedTooltips.top(); + if (last) { + last.value.close(); + openedTooltips.removeTop(); + last = null; + } + } + }); + return function $tooltip(type, prefix, defaultTriggerShow, options) { options = angular.extend({}, defaultOptions, globalOptions, options); @@ -166,6 +178,9 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position']) // By default, the tooltip is not open. // TODO add ability to start tooltip opened ttScope.isOpen = false; + openedTooltips.add(ttScope, { + close: hideTooltipBind + }); function toggleTooltipBind() { if (!ttScope.isOpen) { @@ -413,6 +428,12 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position']) element[0].addEventListener(trigger, hideTooltipBind); }); } + + element.on('keypress', function(e) { + if (e.which === 27) { + hideTooltipBind(); + } + }); }); } }