Remove focus event listeners from un-focusable node
[chromium-blink-merge.git] / ui / login / bubble.js
blob92bf987d51df2430851db0e739994ac8614cacf5
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6  * @fileoverview Bubble implementation.
7  */
9 // TODO(xiyuan): Move this into shared.
10 cr.define('cr.ui', function() {
11   /**
12    * Creates a bubble div.
13    * @constructor
14    * @extends {HTMLDivElement}
15    */
16   var Bubble = cr.ui.define('div');
18   /**
19    * Bubble key codes.
20    * @enum {number}
21    */
22   var KeyCodes = {
23     TAB: 9,
24     ENTER: 13,
25     ESC: 27,
26     SPACE: 32
27   };
29   /**
30    * Bubble attachment side.
31    * @enum {string}
32    */
33   Bubble.Attachment = {
34     RIGHT: 'bubble-right',
35     LEFT: 'bubble-left',
36     TOP: 'bubble-top',
37     BOTTOM: 'bubble-bottom'
38   };
40   Bubble.prototype = {
41     __proto__: HTMLDivElement.prototype,
43     // Anchor element for this bubble.
44     anchor_: undefined,
46     // If defined, sets focus to this element once bubble is closed. Focus is
47     // set to this element only if there's no any other focused element.
48     elementToFocusOnHide_: undefined,
50     // With help of these elements we create closed artificial tab-cycle through
51     // bubble elements.
52     firstBubbleElement_: undefined,
53     lastBubbleElement_: undefined,
55     // Whether to hide bubble when key is pressed.
56     hideOnKeyPress_: true,
58     /** @override */
59     decorate: function() {
60       this.docKeyDownHandler_ = this.handleDocKeyDown_.bind(this);
61       this.selfClickHandler_ = this.handleSelfClick_.bind(this);
62       this.ownerDocument.addEventListener('click',
63                                           this.handleDocClick_.bind(this));
64       this.ownerDocument.addEventListener('keydown',
65                                           this.docKeyDownHandler_);
66       window.addEventListener('blur', this.handleWindowBlur_.bind(this));
67       this.addEventListener('webkitTransitionEnd',
68                             this.handleTransitionEnd_.bind(this));
69       // Guard timer for 200ms + epsilon.
70       ensureTransitionEndEvent(this, 250);
71     },
73     /**
74      * Element that should be focused on hide.
75      * @type {HTMLElement}
76      */
77     set elementToFocusOnHide(value) {
78       this.elementToFocusOnHide_ = value;
79     },
81     /**
82      * Element that should be focused on shift-tab of first bubble element
83      * to create artificial closed tab-cycle through bubble.
84      * Usually close-button.
85      * @type {HTMLElement}
86      */
87     set lastBubbleElement(value) {
88       this.lastBubbleElement_ = value;
89     },
91     /**
92      * Element that should be focused on tab of last bubble element
93      * to create artificial closed tab-cycle through bubble.
94      * Same element as first focused on bubble opening.
95      * @type {HTMLElement}
96      */
97     set firstBubbleElement(value) {
98       this.firstBubbleElement_ = value;
99     },
101     /**
102      * Whether to hide bubble when key is pressed.
103      * @type {boolean}
104      */
105     set hideOnKeyPress(value) {
106       this.hideOnKeyPress_ = value;
107     },
109     /**
110      * Whether to hide bubble when clicked inside bubble element.
111      * Default is true.
112      * @type {boolean}
113      */
114     set hideOnSelfClick(value) {
115       if (value)
116         this.removeEventListener('click', this.selfClickHandler_);
117       else
118         this.addEventListener('click', this.selfClickHandler_);
119     },
121     /**
122      * Handler for click event which prevents bubble auto hide.
123      * @private
124      */
125     handleSelfClick_: function(e) {
126       // Allow clicking on [x] button.
127       if (e.target && e.target.classList.contains('close-button'))
128         return;
129       e.stopPropagation();
130     },
132     /**
133      * Sets the attachment of the bubble.
134      * @param {!Attachment} attachment Bubble attachment.
135      */
136     setAttachment_: function(attachment) {
137       for (var k in Bubble.Attachment) {
138         var v = Bubble.Attachment[k];
139         this.classList.toggle(v, v == attachment);
140       }
141     },
143     /**
144      * Shows the bubble for given anchor element.
145      * @param {!Object} pos Bubble position (left, top, right, bottom in px).
146      * @param {!Attachment} attachment Bubble attachment (on which side of the
147      *     specified position it should be displayed).
148      * @param {HTMLElement} opt_content Content to show in bubble.
149      *     If not specified, bubble element content is shown.
150      * @private
151      */
152     showContentAt_: function(pos, attachment, opt_content) {
153       this.style.top = this.style.left = this.style.right = this.style.bottom =
154           'auto';
155       for (var k in pos) {
156         if (typeof pos[k] == 'number')
157           this.style[k] = pos[k] + 'px';
158       }
159       if (opt_content !== undefined) {
160         this.innerHTML = '';
161         this.appendChild(opt_content);
162       }
163       this.setAttachment_(attachment);
164       this.hidden = false;
165       this.classList.remove('faded');
166     },
168     /**
169      * Shows the bubble for given anchor element. Bubble content is not cleared.
170      * @param {!HTMLElement} el Anchor element of the bubble.
171      * @param {!Attachment} attachment Bubble attachment (on which side of the
172      *     element it should be displayed).
173      * @param {number=} opt_offset Offset of the bubble.
174      * @param {number=} opt_padding Optional padding of the bubble.
175      */
176     showForElement: function(el, attachment, opt_offset, opt_padding) {
177       this.showContentForElement(
178           el, attachment, undefined, opt_offset, opt_padding);
179     },
181     /**
182      * Shows the bubble for given anchor element.
183      * @param {!HTMLElement} el Anchor element of the bubble.
184      * @param {!Attachment} attachment Bubble attachment (on which side of the
185      *     element it should be displayed).
186      * @param {HTMLElement} opt_content Content to show in bubble.
187      *     If not specified, bubble element content is shown.
188      * @param {number=} opt_offset Offset of the bubble attachment point from
189      *     left (for vertical attachment) or top (for horizontal attachment)
190      *     side of the element. If not specified, the bubble is positioned to
191      *     be aligned with the left/top side of the element but not farther than
192      *     half of its width/height.
193      * @param {number=} opt_padding Optional padding of the bubble.
194      */
195     showContentForElement: function(el, attachment, opt_content,
196                                     opt_offset, opt_padding) {
197       /** @const */ var ARROW_OFFSET = 25;
198       /** @const */ var DEFAULT_PADDING = 18;
200       if (opt_padding == undefined)
201         opt_padding = DEFAULT_PADDING;
203       var origin = cr.ui.login.DisplayManager.getPosition(el);
204       var offset = opt_offset == undefined ?
205           [Math.min(ARROW_OFFSET, el.offsetWidth / 2),
206            Math.min(ARROW_OFFSET, el.offsetHeight / 2)] :
207           [opt_offset, opt_offset];
209       var pos = {};
210       if (isRTL()) {
211         switch (attachment) {
212           case Bubble.Attachment.TOP:
213             pos.right = origin.right + offset[0] - ARROW_OFFSET;
214             pos.bottom = origin.bottom + el.offsetHeight + opt_padding;
215             break;
216           case Bubble.Attachment.RIGHT:
217             pos.top = origin.top + offset[1] - ARROW_OFFSET;
218             pos.right = origin.right + el.offsetWidth + opt_padding;
219             break;
220           case Bubble.Attachment.BOTTOM:
221             pos.right = origin.right + offset[0] - ARROW_OFFSET;
222             pos.top = origin.top + el.offsetHeight + opt_padding;
223             break;
224           case Bubble.Attachment.LEFT:
225             pos.top = origin.top + offset[1] - ARROW_OFFSET;
226             pos.left = origin.left + el.offsetWidth + opt_padding;
227             break;
228         }
229       } else {
230         switch (attachment) {
231           case Bubble.Attachment.TOP:
232             pos.left = origin.left + offset[0] - ARROW_OFFSET;
233             pos.bottom = origin.bottom + el.offsetHeight + opt_padding;
234             break;
235           case Bubble.Attachment.RIGHT:
236             pos.top = origin.top + offset[1] - ARROW_OFFSET;
237             pos.left = origin.left + el.offsetWidth + opt_padding;
238             break;
239           case Bubble.Attachment.BOTTOM:
240             pos.left = origin.left + offset[0] - ARROW_OFFSET;
241             pos.top = origin.top + el.offsetHeight + opt_padding;
242             break;
243           case Bubble.Attachment.LEFT:
244             pos.top = origin.top + offset[1] - ARROW_OFFSET;
245             pos.right = origin.right + el.offsetWidth + opt_padding;
246             break;
247         }
248       }
250       this.anchor_ = el;
251       this.showContentAt_(pos, attachment, opt_content);
252     },
254     /**
255      * Shows the bubble for given anchor element.
256      * @param {!HTMLElement} el Anchor element of the bubble.
257      * @param {string} text Text content to show in bubble.
258      * @param {!Attachment} attachment Bubble attachment (on which side of the
259      *     element it should be displayed).
260      * @param {number=} opt_offset Offset of the bubble attachment point from
261      *     left (for vertical attachment) or top (for horizontal attachment)
262      *     side of the element. If not specified, the bubble is positioned to
263      *     be aligned with the left/top side of the element but not farther than
264      *     half of its weight/height.
265      * @param {number=} opt_padding Optional padding of the bubble.
266      */
267     showTextForElement: function(el, text, attachment,
268                                  opt_offset, opt_padding) {
269       var span = this.ownerDocument.createElement('span');
270       span.textContent = text;
271       this.showContentForElement(el, attachment, span, opt_offset, opt_padding);
272     },
274     /**
275      * Hides the bubble.
276      */
277     hide: function() {
278       if (!this.classList.contains('faded'))
279         this.classList.add('faded');
280     },
282     /**
283      * Hides the bubble anchored to the given element (if any).
284      * @param {!Object} el Anchor element.
285      */
286     hideForElement: function(el) {
287       if (!this.hidden && this.anchor_ == el)
288         this.hide();
289     },
291     /**
292      * Handler for faded transition end.
293      * @private
294      */
295     handleTransitionEnd_: function(e) {
296       if (this.classList.contains('faded')) {
297         this.hidden = true;
298         if (this.elementToFocusOnHide_)
299           this.elementToFocusOnHide_.focus();
300       }
301     },
303     /**
304      * Handler of document click event.
305      * @private
306      */
307     handleDocClick_: function(e) {
308       // Ignore clicks on anchor element.
309       if (e.target == this.anchor_)
310         return;
312       if (!this.hidden)
313         this.hide();
314     },
316     /**
317      * Handle of document keydown event.
318      * @private
319      */
320     handleDocKeyDown_: function(e) {
321       if (this.hidden)
322         return;
324       if (this.hideOnKeyPress_) {
325         this.hide();
326         return;
327       }
328       // Artificial tab-cycle.
329       if (e.keyCode == KeyCodes.TAB && e.shiftKey == true &&
330           e.target == this.firstBubbleElement_) {
331         this.lastBubbleElement_.focus();
332         e.preventDefault();
333       }
334       if (e.keyCode == KeyCodes.TAB && e.shiftKey == false &&
335           e.target == this.lastBubbleElement_) {
336         this.firstBubbleElement_.focus();
337         e.preventDefault();
338       }
339       // Close bubble on ESC or on hitting spacebar or Enter at close-button.
340       if (e.keyCode == KeyCodes.ESC ||
341           ((e.keyCode == KeyCodes.ENTER || e.keyCode == KeyCodes.SPACE) &&
342              e.target && e.target.classList.contains('close-button')))
343         this.hide();
344     },
346     /**
347      * Handler of window blur event.
348      * @private
349      */
350     handleWindowBlur_: function(e) {
351       if (!this.hidden)
352         this.hide();
353     }
354   };
356   return {
357     Bubble: Bubble
358   };