OLD | NEW |
1 /** | 1 /** |
2 Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or
shown, and displays | 2 Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or
shown, and displays |
3 on top of other content. It includes an optional backdrop, and can be used to im
plement a variety | 3 on top of other content. It includes an optional backdrop, and can be used to im
plement a variety |
4 of UI controls including dialogs and drop downs. Multiple overlays may be displa
yed at once. | 4 of UI controls including dialogs and drop downs. Multiple overlays may be displa
yed at once. |
5 | 5 |
6 ### Closing and canceling | 6 ### Closing and canceling |
7 | 7 |
8 A dialog may be hidden by closing or canceling. The difference between close and
cancel is user | 8 A dialog may be hidden by closing or canceling. The difference between close and
cancel is user |
9 intent. Closing generally implies that the user acknowledged the content on the
overlay. By default, | 9 intent. Closing generally implies that the user acknowledged the content on the
overlay. By default, |
10 it will cancel whenever the user taps outside it or presses the escape key. This
behavior is | 10 it will cancel whenever the user taps outside it or presses the escape key. This
behavior is |
(...skipping 82 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
93 | 93 |
94 /** | 94 /** |
95 * Returns the reason this dialog was last closed. | 95 * Returns the reason this dialog was last closed. |
96 */ | 96 */ |
97 closingReason: { | 97 closingReason: { |
98 // was a getter before, but needs to be a property so other | 98 // was a getter before, but needs to be a property so other |
99 // behaviors can override this. | 99 // behaviors can override this. |
100 type: Object | 100 type: Object |
101 }, | 101 }, |
102 | 102 |
| 103 /** |
| 104 * The HTMLElement that will be firing relevant KeyboardEvents. |
| 105 * Used for capturing esc and tab. Overridden from `IronA11yKeysBehavior`. |
| 106 */ |
| 107 keyEventTarget: { |
| 108 type: Object, |
| 109 value: document |
| 110 }, |
| 111 |
| 112 /** |
| 113 * Set to true to enable restoring of focus when overlay is closed. |
| 114 */ |
| 115 restoreFocusOnClose: { |
| 116 type: Boolean, |
| 117 value: false |
| 118 }, |
| 119 |
103 _manager: { | 120 _manager: { |
104 type: Object, | 121 type: Object, |
105 value: Polymer.IronOverlayManager | 122 value: Polymer.IronOverlayManager |
106 }, | 123 }, |
107 | 124 |
108 _boundOnCaptureClick: { | 125 _boundOnCaptureClick: { |
109 type: Function, | 126 type: Function, |
110 value: function() { | 127 value: function() { |
111 return this._onCaptureClick.bind(this); | 128 return this._onCaptureClick.bind(this); |
112 } | 129 } |
113 }, | 130 }, |
114 | 131 |
115 _boundOnCaptureKeydown: { | |
116 type: Function, | |
117 value: function() { | |
118 return this._onCaptureKeydown.bind(this); | |
119 } | |
120 }, | |
121 | |
122 _boundOnCaptureFocus: { | 132 _boundOnCaptureFocus: { |
123 type: Function, | 133 type: Function, |
124 value: function() { | 134 value: function() { |
125 return this._onCaptureFocus.bind(this); | 135 return this._onCaptureFocus.bind(this); |
126 } | 136 } |
127 }, | 137 }, |
128 | 138 |
129 /** @type {?Node} */ | 139 /** |
| 140 * The node being focused. |
| 141 * @type {?Node} |
| 142 */ |
130 _focusedChild: { | 143 _focusedChild: { |
131 type: Object | 144 type: Object |
132 } | 145 } |
133 | 146 |
134 }, | 147 }, |
135 | 148 |
| 149 keyBindings: { |
| 150 'esc': '__onEsc', |
| 151 'tab': '__onTab' |
| 152 }, |
| 153 |
136 listeners: { | 154 listeners: { |
137 'iron-resize': '_onIronResize' | 155 'iron-resize': '_onIronResize' |
138 }, | 156 }, |
139 | 157 |
140 /** | 158 /** |
141 * The backdrop element. | 159 * The backdrop element. |
142 * @type Node | 160 * @type {Node} |
143 */ | 161 */ |
144 get backdropElement() { | 162 get backdropElement() { |
145 return this._manager.backdropElement; | 163 return this._manager.backdropElement; |
146 }, | 164 }, |
147 | 165 |
| 166 /** |
| 167 * Returns the node to give focus to. |
| 168 * @type {Node} |
| 169 */ |
148 get _focusNode() { | 170 get _focusNode() { |
149 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]'
) || this; | 171 return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]'
) || this; |
150 }, | 172 }, |
151 | 173 |
| 174 /** |
| 175 * Array of nodes that can receive focus (overlay included), ordered by `tab
index`. |
| 176 * This is used to retrieve which is the first and last focusable nodes in o
rder |
| 177 * to wrap the focus for overlays `with-backdrop`. |
| 178 * |
| 179 * If you know what is your content (specifically the first and last focusab
le children), |
| 180 * you can override this method to return only `[firstFocusable, lastFocusab
le];` |
| 181 * @type {[Node]} |
| 182 * @protected |
| 183 */ |
| 184 get _focusableNodes() { |
| 185 // Elements that can be focused even if they have [disabled] attribute. |
| 186 var FOCUSABLE_WITH_DISABLED = [ |
| 187 'a[href]', |
| 188 'area[href]', |
| 189 'iframe', |
| 190 '[tabindex]', |
| 191 '[contentEditable=true]' |
| 192 ]; |
| 193 |
| 194 // Elements that cannot be focused if they have [disabled] attribute. |
| 195 var FOCUSABLE_WITHOUT_DISABLED = [ |
| 196 'input', |
| 197 'select', |
| 198 'textarea', |
| 199 'button' |
| 200 ]; |
| 201 |
| 202 // Discard elements with tabindex=-1 (makes them not focusable). |
| 203 var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') + |
| 204 ':not([tabindex="-1"]),' + |
| 205 FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),'
) + |
| 206 ':not([disabled]):not([tabindex="-1"])'; |
| 207 |
| 208 var focusables = Polymer.dom(this).querySelectorAll(selector); |
| 209 if (this.tabIndex >= 0) { |
| 210 // Insert at the beginning because we might have all elements with tabIn
dex = 0, |
| 211 // and the overlay should be the first of the list. |
| 212 focusables.splice(0, 0, this); |
| 213 } |
| 214 // Sort by tabindex. |
| 215 return focusables.sort(function (a, b) { |
| 216 if (a.tabIndex === b.tabIndex) { |
| 217 return 0; |
| 218 } |
| 219 if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) { |
| 220 return 1; |
| 221 } |
| 222 return -1; |
| 223 }); |
| 224 }, |
| 225 |
152 ready: function() { | 226 ready: function() { |
153 // with-backdrop need tabindex to be set in order to trap the focus. | 227 // with-backdrop needs tabindex to be set in order to trap the focus. |
154 // If it is not set, IronOverlayBehavior will set it, and remove it if wit
h-backdrop = false. | 228 // If it is not set, IronOverlayBehavior will set it, and remove it if wit
h-backdrop = false. |
155 this.__shouldRemoveTabIndex = false; | 229 this.__shouldRemoveTabIndex = false; |
| 230 // Used for wrapping the focus on TAB / Shift+TAB. |
| 231 this.__firstFocusableNode = this.__lastFocusableNode = null; |
156 this._ensureSetup(); | 232 this._ensureSetup(); |
157 }, | 233 }, |
158 | 234 |
159 attached: function() { | 235 attached: function() { |
160 // Call _openedChanged here so that position can be computed correctly. | 236 // Call _openedChanged here so that position can be computed correctly. |
161 if (this._callOpenedWhenReady) { | 237 if (this.opened) { |
162 this._openedChanged(); | 238 this._openedChanged(); |
163 } | 239 } |
| 240 this._observer = Polymer.dom(this).observeNodes(this._onNodesChange); |
164 }, | 241 }, |
165 | 242 |
166 detached: function() { | 243 detached: function() { |
| 244 Polymer.dom(this).unobserveNodes(this._observer); |
| 245 this._observer = null; |
167 this.opened = false; | 246 this.opened = false; |
168 this._manager.trackBackdrop(this); | 247 this._manager.trackBackdrop(this); |
169 this._manager.removeOverlay(this); | 248 this._manager.removeOverlay(this); |
170 }, | 249 }, |
171 | 250 |
172 /** | 251 /** |
173 * Toggle the opened state of the overlay. | 252 * Toggle the opened state of the overlay. |
174 */ | 253 */ |
175 toggle: function() { | 254 toggle: function() { |
176 this._setCanceled(false); | 255 this._setCanceled(false); |
(...skipping 11 matching lines...) Expand all Loading... |
188 /** | 267 /** |
189 * Close the overlay. | 268 * Close the overlay. |
190 */ | 269 */ |
191 close: function() { | 270 close: function() { |
192 this._setCanceled(false); | 271 this._setCanceled(false); |
193 this.opened = false; | 272 this.opened = false; |
194 }, | 273 }, |
195 | 274 |
196 /** | 275 /** |
197 * Cancels the overlay. | 276 * Cancels the overlay. |
| 277 * @param {?Event} event The original event |
198 */ | 278 */ |
199 cancel: function() { | 279 cancel: function(event) { |
200 var cancelEvent = this.fire('iron-overlay-canceled', undefined, {cancelabl
e: true}); | 280 var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: t
rue}); |
201 if (cancelEvent.defaultPrevented) { | 281 if (cancelEvent.defaultPrevented) { |
202 return; | 282 return; |
203 } | 283 } |
204 | 284 |
205 this._setCanceled(true); | 285 this._setCanceled(true); |
206 this.opened = false; | 286 this.opened = false; |
207 }, | 287 }, |
208 | 288 |
209 _ensureSetup: function() { | 289 _ensureSetup: function() { |
210 if (this._overlaySetup) { | 290 if (this._overlaySetup) { |
211 return; | 291 return; |
212 } | 292 } |
213 this._overlaySetup = true; | 293 this._overlaySetup = true; |
214 this.style.outline = 'none'; | 294 this.style.outline = 'none'; |
215 this.style.display = 'none'; | 295 this.style.display = 'none'; |
216 }, | 296 }, |
217 | 297 |
218 _openedChanged: function() { | 298 _openedChanged: function() { |
219 if (this.opened) { | 299 if (this.opened) { |
220 this.removeAttribute('aria-hidden'); | 300 this.removeAttribute('aria-hidden'); |
221 } else { | 301 } else { |
222 this.setAttribute('aria-hidden', 'true'); | 302 this.setAttribute('aria-hidden', 'true'); |
223 Polymer.dom(this).unobserveNodes(this._observer); | |
224 } | 303 } |
225 | 304 |
226 // wait to call after ready only if we're initially open | 305 // wait to call after ready only if we're initially open |
227 if (!this._overlaySetup) { | 306 if (!this._overlaySetup) { |
228 this._callOpenedWhenReady = this.opened; | |
229 return; | 307 return; |
230 } | 308 } |
231 | 309 |
232 this._manager.trackBackdrop(this); | 310 this._manager.trackBackdrop(this); |
233 | 311 |
234 if (this.opened) { | 312 if (this.opened) { |
235 this._prepareRenderOpened(); | 313 this._prepareRenderOpened(); |
236 } | 314 } |
237 | 315 |
238 if (this._openChangedAsync) { | 316 if (this._openChangedAsync) { |
(...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
293 node.addEventListener(event, boundListener, capture); | 371 node.addEventListener(event, boundListener, capture); |
294 } else { | 372 } else { |
295 // disable document-wide tap recognizer | 373 // disable document-wide tap recognizer |
296 if (event === 'tap') { | 374 if (event === 'tap') { |
297 Polymer.Gestures.remove(document, 'tap', null); | 375 Polymer.Gestures.remove(document, 'tap', null); |
298 } | 376 } |
299 node.removeEventListener(event, boundListener, capture); | 377 node.removeEventListener(event, boundListener, capture); |
300 } | 378 } |
301 }, | 379 }, |
302 | 380 |
303 _toggleListeners: function () { | 381 _toggleListeners: function() { |
304 this._toggleListener(this.opened, document, 'tap', this._boundOnCaptureCli
ck, true); | 382 this._toggleListener(this.opened, document, 'tap', this._boundOnCaptureCli
ck, true); |
305 this._toggleListener(this.opened, document, 'keydown', this._boundOnCaptur
eKeydown, true); | |
306 this._toggleListener(this.opened, document, 'focus', this._boundOnCaptureF
ocus, true); | 383 this._toggleListener(this.opened, document, 'focus', this._boundOnCaptureF
ocus, true); |
307 }, | 384 }, |
308 | 385 |
309 // tasks which must occur before opening; e.g. making the element visible | 386 // tasks which must occur before opening; e.g. making the element visible |
310 _prepareRenderOpened: function() { | 387 _prepareRenderOpened: function() { |
| 388 |
311 this._manager.addOverlay(this); | 389 this._manager.addOverlay(this); |
312 | 390 |
| 391 // Needed to calculate the size of the overlay so that transitions on its
size |
| 392 // will have the correct starting points. |
313 this._preparePositioning(); | 393 this._preparePositioning(); |
314 this.fit(); | 394 this.fit(); |
315 this._finishPositioning(); | 395 this._finishPositioning(); |
316 | 396 |
317 if (this.withBackdrop) { | 397 if (this.withBackdrop) { |
318 this.backdropElement.prepare(); | 398 this.backdropElement.prepare(); |
319 } | 399 } |
| 400 |
| 401 // Safari will apply the focus to the autofocus element when displayed for
the first time, |
| 402 // so we blur it. Later, _applyFocus will set the focus if necessary. |
| 403 if (this.noAutoFocus && document.activeElement === this._focusNode) { |
| 404 this._focusNode.blur(); |
| 405 } |
320 }, | 406 }, |
321 | 407 |
322 // tasks which cause the overlay to actually open; typically play an | 408 // tasks which cause the overlay to actually open; typically play an |
323 // animation | 409 // animation |
324 _renderOpened: function() { | 410 _renderOpened: function() { |
325 if (this.withBackdrop) { | 411 if (this.withBackdrop) { |
326 this.backdropElement.open(); | 412 this.backdropElement.open(); |
327 } | 413 } |
328 this._finishRenderOpened(); | 414 this._finishRenderOpened(); |
329 }, | 415 }, |
330 | 416 |
331 _renderClosed: function() { | 417 _renderClosed: function() { |
332 if (this.withBackdrop) { | 418 if (this.withBackdrop) { |
333 this.backdropElement.close(); | 419 this.backdropElement.close(); |
334 } | 420 } |
335 this._finishRenderClosed(); | 421 this._finishRenderClosed(); |
336 }, | 422 }, |
337 | 423 |
338 _finishRenderOpened: function() { | 424 _finishRenderOpened: function() { |
339 // focus the child node with [autofocus] | 425 // This ensures the overlay is visible before we set the focus |
| 426 // (by calling _onIronResize -> refit). |
| 427 this.notifyResize(); |
| 428 // Focus the child node with [autofocus] |
340 this._applyFocus(); | 429 this._applyFocus(); |
341 | 430 |
342 this._observer = Polymer.dom(this).observeNodes(this.notifyResize); | |
343 this.fire('iron-overlay-opened'); | 431 this.fire('iron-overlay-opened'); |
344 }, | 432 }, |
345 | 433 |
346 _finishRenderClosed: function() { | 434 _finishRenderClosed: function() { |
347 // hide the overlay and remove the backdrop | 435 // Hide the overlay and remove the backdrop. |
348 this.resetFit(); | 436 this.resetFit(); |
349 this.style.display = 'none'; | 437 this.style.display = 'none'; |
350 this._manager.removeOverlay(this); | 438 this._manager.removeOverlay(this); |
351 | 439 |
352 this._focusedChild = null; | |
353 this._applyFocus(); | 440 this._applyFocus(); |
| 441 this.notifyResize(); |
354 | 442 |
355 this.notifyResize(); | |
356 this.fire('iron-overlay-closed', this.closingReason); | 443 this.fire('iron-overlay-closed', this.closingReason); |
357 }, | 444 }, |
358 | 445 |
359 _preparePositioning: function() { | 446 _preparePositioning: function() { |
360 this.style.transition = this.style.webkitTransition = 'none'; | 447 this.style.transition = this.style.webkitTransition = 'none'; |
361 this.style.transform = this.style.webkitTransform = 'none'; | 448 this.style.transform = this.style.webkitTransform = 'none'; |
362 this.style.display = ''; | 449 this.style.display = ''; |
363 }, | 450 }, |
364 | 451 |
365 _finishPositioning: function() { | 452 _finishPositioning: function() { |
366 this.style.display = 'none'; | 453 this.style.display = 'none'; |
367 this.style.transform = this.style.webkitTransform = ''; | 454 this.style.transform = this.style.webkitTransform = ''; |
368 // force layout to avoid application of transform | 455 // Force layout layout to avoid application of transform. |
369 /** @suppress {suspiciousCode} */ this.offsetWidth; | 456 // Set offsetWidth to itself so that compilers won't remove it. |
| 457 this.offsetWidth = this.offsetWidth; |
370 this.style.transition = this.style.webkitTransition = ''; | 458 this.style.transition = this.style.webkitTransition = ''; |
371 }, | 459 }, |
372 | 460 |
373 _applyFocus: function() { | 461 _applyFocus: function() { |
374 if (this.opened) { | 462 if (this.opened) { |
375 if (!this.noAutoFocus) { | 463 if (!this.noAutoFocus) { |
376 this._focusNode.focus(); | 464 this._focusNode.focus(); |
377 } | 465 } |
378 } else { | 466 } else { |
379 this._focusNode.blur(); | 467 this._focusNode.blur(); |
| 468 this._focusedChild = null; |
380 this._manager.focusOverlay(); | 469 this._manager.focusOverlay(); |
381 } | 470 } |
382 }, | 471 }, |
383 | 472 |
384 _onCaptureClick: function(event) { | 473 _onCaptureClick: function(event) { |
385 if (this._manager.currentOverlay() === this && | 474 if (this._manager.currentOverlay() === this && |
386 Polymer.dom(event).path.indexOf(this) === -1) { | 475 Polymer.dom(event).path.indexOf(this) === -1) { |
387 if (this.noCancelOnOutsideClick) { | 476 if (this.noCancelOnOutsideClick) { |
388 this._applyFocus(); | 477 this._applyFocus(); |
389 } else { | 478 } else { |
390 this.cancel(); | 479 this.cancel(event); |
391 } | 480 } |
392 } | 481 } |
393 }, | 482 }, |
394 | 483 |
395 _onCaptureKeydown: function(event) { | |
396 var ESC = 27; | |
397 if (this._manager.currentOverlay() === this && | |
398 !this.noCancelOnEscKey && | |
399 event.keyCode === ESC) { | |
400 this.cancel(); | |
401 } | |
402 }, | |
403 | |
404 _onCaptureFocus: function (event) { | 484 _onCaptureFocus: function (event) { |
405 if (this._manager.currentOverlay() === this && | 485 if (this._manager.currentOverlay() === this && this.withBackdrop) { |
406 this.withBackdrop) { | |
407 var path = Polymer.dom(event).path; | 486 var path = Polymer.dom(event).path; |
408 if (path.indexOf(this) === -1) { | 487 if (path.indexOf(this) === -1) { |
409 event.stopPropagation(); | 488 event.stopPropagation(); |
410 this._applyFocus(); | 489 this._applyFocus(); |
411 } else { | 490 } else { |
412 this._focusedChild = path[0]; | 491 this._focusedChild = path[0]; |
413 } | 492 } |
414 } | 493 } |
415 }, | 494 }, |
416 | 495 |
417 _onIronResize: function() { | 496 _onIronResize: function() { |
418 if (this.opened) { | 497 if (this.opened) { |
419 this.refit(); | 498 this.refit(); |
420 } | 499 } |
| 500 }, |
| 501 |
| 502 /** |
| 503 * @protected |
| 504 * Will call notifyResize if overlay is opened. |
| 505 * Can be overridden in order to avoid multiple observers on the same node. |
| 506 */ |
| 507 _onNodesChange: function() { |
| 508 if (this.opened) { |
| 509 this.notifyResize(); |
| 510 } |
| 511 // Store it so we don't query too much. |
| 512 var focusableNodes = this._focusableNodes; |
| 513 this.__firstFocusableNode = focusableNodes[0]; |
| 514 this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1]; |
| 515 }, |
| 516 |
| 517 __onEsc: function(event) { |
| 518 // Not opened or not on top, so return. |
| 519 if (this._manager.currentOverlay() !== this) { |
| 520 return; |
| 521 } |
| 522 if (!this.noCancelOnEscKey) { |
| 523 this.cancel(event); |
| 524 } |
| 525 }, |
| 526 |
| 527 __onTab: function(event) { |
| 528 // Not opened or not on top, so return. |
| 529 if (this._manager.currentOverlay() !== this) { |
| 530 return; |
| 531 } |
| 532 // TAB wraps from last to first focusable. |
| 533 // Shift + TAB wraps from first to last focusable. |
| 534 var shift = event.detail.keyboardEvent.shiftKey; |
| 535 var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusable
Node; |
| 536 var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNo
de; |
| 537 if (this.withBackdrop && this._focusedChild === nodeToCheck) { |
| 538 // We set here the _focusedChild so that _onCaptureFocus will handle the |
| 539 // wrapping of the focus (the next event after tab is focus). |
| 540 this._focusedChild = nodeToSet; |
| 541 } |
421 } | 542 } |
422 | |
423 /** | |
424 * Fired after the `iron-overlay` opens. | |
425 * @event iron-overlay-opened | |
426 */ | |
427 | |
428 /** | |
429 * Fired when the `iron-overlay` is canceled, but before it is closed. | |
430 * Cancel the event to prevent the `iron-overlay` from closing. | |
431 * @event iron-overlay-canceled | |
432 */ | |
433 | |
434 /** | |
435 * Fired after the `iron-overlay` closes. | |
436 * @event iron-overlay-closed | |
437 * @param {{canceled: (boolean|undefined)}} set to the `closingReason` attribute | |
438 */ | |
439 }; | 543 }; |
440 | 544 |
441 /** @polymerBehavior */ | 545 /** @polymerBehavior */ |
442 Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableB
ehavior, Polymer.IronOverlayBehaviorImpl]; | 546 Polymer.IronOverlayBehavior = [Polymer.IronA11yKeysBehavior, Polymer.IronFitBe
havior, Polymer.IronResizableBehavior, Polymer.IronOverlayBehaviorImpl]; |
| 547 |
| 548 /** |
| 549 * Fired after the `iron-overlay` opens. |
| 550 * @event iron-overlay-opened |
| 551 */ |
| 552 |
| 553 /** |
| 554 * Fired when the `iron-overlay` is canceled, but before it is closed. |
| 555 * Cancel the event to prevent the `iron-overlay` from closing. |
| 556 * @event iron-overlay-canceled |
| 557 * @param {Event} event The closing of the `iron-overlay` can be prevented |
| 558 * by calling `event.preventDefault()`. The `event.detail` is the original even
t that originated |
| 559 * the canceling (e.g. ESC keyboard event or click event outside the `iron-over
lay`). |
| 560 */ |
| 561 |
| 562 /** |
| 563 * Fired after the `iron-overlay` closes. |
| 564 * @event iron-overlay-closed |
| 565 * @param {{canceled: (boolean|undefined)}} closingReason Contains `canceled` (
whether the overlay was canceled). |
| 566 */ |
OLD | NEW |