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.
4
5/**
6 * @fileoverview User pod row implementation.
7 */
8
9cr.define('login', function() {
10  /**
11   * Number of displayed columns depending on user pod count.
12   * @type {Array.<number>}
13   * @const
14   */
15  var COLUMNS = [0, 1, 2, 3, 4, 5, 4, 4, 4, 5, 5, 6, 6, 5, 5, 6, 6, 6, 6];
16
17  /**
18   * Mapping between number of columns in pod-row and margin between user pods
19   * for such layout.
20   * @type {Array.<number>}
21   * @const
22   */
23  var MARGIN_BY_COLUMNS = [undefined, 40, 40, 40, 40, 40, 12];
24
25  /**
26   * Mapping between number of columns in the desktop pod-row and margin
27   * between user pods for such layout.
28   * @type {Array.<number>}
29   * @const
30   */
31  var DESKTOP_MARGIN_BY_COLUMNS = [undefined, 15, 15, 15, 15, 15, 15];
32
33  /**
34   * Maximal number of columns currently supported by pod-row.
35   * @type {number}
36   * @const
37   */
38  var MAX_NUMBER_OF_COLUMNS = 6;
39
40  /**
41   * Maximal number of rows if sign-in banner is displayed alonside.
42   * @type {number}
43   * @const
44   */
45  var MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER = 2;
46
47  /**
48   * Variables used for pod placement processing. Width and height should be
49   * synced with computed CSS sizes of pods.
50   */
51  var POD_WIDTH = 180;
52  var PUBLIC_EXPANDED_BASIC_WIDTH = 500;
53  var PUBLIC_EXPANDED_ADVANCED_WIDTH = 610;
54  var CROS_POD_HEIGHT = 213;
55  var DESKTOP_POD_HEIGHT = 226;
56  var POD_ROW_PADDING = 10;
57  var DESKTOP_ROW_PADDING = 15;
58  var CUSTOM_ICON_CONTAINER_SIZE = 40;
59
60  /**
61   * Minimal padding between user pod and virtual keyboard.
62   * @type {number}
63   * @const
64   */
65  var USER_POD_KEYBOARD_MIN_PADDING = 20;
66
67  /**
68   * Maximum time for which the pod row remains hidden until all user images
69   * have been loaded.
70   * @type {number}
71   * @const
72   */
73  var POD_ROW_IMAGES_LOAD_TIMEOUT_MS = 3000;
74
75  /**
76   * Public session help topic identifier.
77   * @type {number}
78   * @const
79   */
80  var HELP_TOPIC_PUBLIC_SESSION = 3041033;
81
82  /**
83   * Tab order for user pods. Update these when adding new controls.
84   * @enum {number}
85   * @const
86   */
87  var UserPodTabOrder = {
88    POD_INPUT: 1,        // Password input fields (and whole pods themselves).
89    POD_CUSTOM_ICON: 2,  // Pod custom icon next to passwrod input field.
90    HEADER_BAR: 3,       // Buttons on the header bar (Shutdown, Add User).
91    ACTION_BOX: 4,       // Action box buttons.
92    PAD_MENU_ITEM: 5     // User pad menu items (Remove this user).
93  };
94
95  /**
96   * Supported authentication types. Keep in sync with the enum in
97   * chrome/browser/signin/screenlock_bridge.h
98   * @enum {number}
99   * @const
100   */
101  var AUTH_TYPE = {
102    OFFLINE_PASSWORD: 0,
103    ONLINE_SIGN_IN: 1,
104    NUMERIC_PIN: 2,
105    USER_CLICK: 3,
106    EXPAND_THEN_USER_CLICK: 4,
107    FORCE_OFFLINE_PASSWORD: 5
108  };
109
110  /**
111   * Names of authentication types.
112   */
113  var AUTH_TYPE_NAMES = {
114    0: 'offlinePassword',
115    1: 'onlineSignIn',
116    2: 'numericPin',
117    3: 'userClick',
118    4: 'expandThenUserClick',
119    5: 'forceOfflinePassword'
120  };
121
122  // Focus and tab order are organized as follows:
123  //
124  // (1) all user pods have tab index 1 so they are traversed first;
125  // (2) when a user pod is activated, its tab index is set to -1 and its
126  // main input field gets focus and tab index 1;
127  // (3) if user pod custom icon is interactive, it has tab index 2 so it
128  // follows the input.
129  // (4) buttons on the header bar have tab index 3 so they follow the custom
130  // icon, or user pod if custom icon is not interactive;
131  // (5) Action box buttons have tab index 4 and follow header bar buttons;
132  // (6) lastly, focus jumps to the Status Area and back to user pods.
133  //
134  // 'Focus' event is handled by a capture handler for the whole document
135  // and in some cases 'mousedown' event handlers are used instead of 'click'
136  // handlers where it's necessary to prevent 'focus' event from being fired.
137
138  /**
139   * Helper function to remove a class from given element.
140   * @param {!HTMLElement} el Element whose class list to change.
141   * @param {string} cl Class to remove.
142   */
143  function removeClass(el, cl) {
144    el.classList.remove(cl);
145  }
146
147  /**
148   * Creates a user pod.
149   * @constructor
150   * @extends {HTMLDivElement}
151   */
152  var UserPod = cr.ui.define(function() {
153    var node = $('user-pod-template').cloneNode(true);
154    node.removeAttribute('id');
155    return node;
156  });
157
158  /**
159   * Stops event propagation from the any user pod child element.
160   * @param {Event} e Event to handle.
161   */
162  function stopEventPropagation(e) {
163    // Prevent default so that we don't trigger a 'focus' event.
164    e.preventDefault();
165    e.stopPropagation();
166  }
167
168  /**
169   * Creates an element for custom icon shown in a user pod next to the input
170   * field.
171   * @constructor
172   * @extends {HTMLDivElement}
173   */
174  var UserPodCustomIcon = cr.ui.define(function() {
175    var node = document.createElement('div');
176    node.classList.add('custom-icon-container');
177    node.hidden = true;
178
179    // Create the actual icon element and add it as a child to the container.
180    var iconNode = document.createElement('div');
181    iconNode.classList.add('custom-icon');
182    node.appendChild(iconNode);
183    return node;
184  });
185
186  /**
187   * The supported user pod custom icons.
188   * {@code id} properties should be in sync with values set by C++ side.
189   * {@code class} properties are CSS classes used to set the icons' background.
190   * @const {Array.<{id: !string, class: !string}>}
191   */
192  UserPodCustomIcon.ICONS = [
193    {id: 'locked', class: 'custom-icon-locked'},
194    {id: 'unlocked', class: 'custom-icon-unlocked'},
195    {id: 'hardlocked', class: 'custom-icon-hardlocked'},
196    {id: 'spinner', class: 'custom-icon-spinner'}
197  ];
198
199  UserPodCustomIcon.prototype = {
200    __proto__: HTMLDivElement.prototype,
201
202    /**
203     * The id of the icon being shown.
204     * @type {string}
205     * @private
206     */
207    iconId_: '',
208
209    /**
210     * Tooltip to be shown when the user hovers over the icon. The icon
211     * properties may be set so the tooltip is shown automatically when the icon
212     * is updated. The tooltip is shown in a bubble attached to the icon
213     * element.
214     * @type {string}
215     * @private
216     */
217    tooltip_: '',
218
219    /**
220     * Whether the tooltip is shown and the user is hovering over the icon.
221     * @type {boolean}
222     * @private
223     */
224    tooltipActive_: false,
225
226    /**
227     * Whether the icon has been shown as a result of |autoshow| parameter begin
228     * set rather than user hovering over the icon.
229     * If this is set, the tooltip will not be removed when the mouse leaves the
230     * icon.
231     * @type {boolean}
232     * @private
233     */
234    tooltipAutoshown_: false,
235
236    /**
237     * A reference to the timeout for showing tooltip after mouse enters the
238     * icon.
239     * @type {?number}
240     * @private
241     */
242    showTooltipTimeout_: null,
243
244    /**
245     * When {@code fadeOut} is called, the element gets hidden using fadeout
246     * animation. This is reference to the listener for transition end added to
247     * the icon element while it's fading out.
248     * @type {?function(Event)}
249     * @private
250     */
251    hideTransitionListener_: null,
252
253    /**
254     * Callback for click and 'Enter' key events that gets set if the icon is
255     * interactive.
256     * @type {?function()}
257     * @private
258     */
259    actionHandler_: null,
260
261    /** @override */
262    decorate: function() {
263      this.iconElement.addEventListener('mouseover',
264                                        this.showTooltipSoon_.bind(this));
265      this.iconElement.addEventListener('mouseout',
266                                        this.hideTooltip_.bind(this, false));
267      this.iconElement.addEventListener('mousedown',
268                                        this.handleMouseDown_.bind(this));
269      this.iconElement.addEventListener('click',
270                                        this.handleClick_.bind(this));
271      this.iconElement.addEventListener('keydown',
272                                        this.handleKeyDown_.bind(this));
273
274      // When the icon is focused using mouse, there should be no outline shown.
275      // Preventing default mousedown event accomplishes this.
276      this.iconElement.addEventListener('mousedown', function(e) {
277        e.preventDefault();
278      });
279    },
280
281    /**
282     * Getter for the icon element's div.
283     * @return {HTMLDivElement}
284     */
285    get iconElement() {
286      return this.querySelector('.custom-icon');
287    },
288
289    /**
290     * Updates the icon element class list to properly represent the provided
291     * icon.
292     * @param {!string} id The id of the icon that should be shown. Should be
293     *    one of the ids listed in {@code UserPodCustomIcon.ICONS}.
294     */
295    setIcon: function(id) {
296      this.iconId_ = id;
297      UserPodCustomIcon.ICONS.forEach(function(icon) {
298        this.iconElement.classList.toggle(icon.class, id == icon.id);
299      }, this);
300    },
301
302    /**
303     * Sets the ARIA label for the icon.
304     * @param {!string} ariaLabel
305     */
306    setAriaLabel: function(ariaLabel) {
307      this.iconElement.setAttribute('aria-label', ariaLabel);
308    },
309
310    /**
311     * Shows the icon.
312     */
313    show: function() {
314      this.resetHideTransitionState_();
315      this.hidden = false;
316    },
317
318    /**
319     * Hides the icon using a fade-out animation.
320     */
321    fadeOut: function() {
322      // The icon is already being hidden.
323      if (this.iconElement.classList.contains('faded'))
324        return;
325
326      this.hideTooltip_(true);
327      this.iconElement.classList.add('faded');
328      this.hideTransitionListener_ = this.hide.bind(this);
329      this.iconElement.addEventListener('webkitTransitionEnd',
330                                        this.hideTransitionListener_);
331      ensureTransitionEndEvent(this.iconElement, 200);
332    },
333
334    /**
335     * Updates the icon tooltip. If {@code autoshow} parameter is set the
336     * tooltip is immediatelly shown. If tooltip text is not set, the method
337     * ensures the tooltip gets hidden. If tooltip is shown prior to this call,
338     * it remains shown, but the tooltip text is updated.
339     * @param {!{text: string, autoshow: boolean}} tooltip The tooltip
340     *    parameters.
341     */
342    setTooltip: function(tooltip) {
343      this.iconElement.classList.toggle('icon-with-tooltip', !!tooltip.text);
344
345      if (this.tooltip_ == tooltip.text && !tooltip.autoshow)
346        return;
347      this.tooltip_ = tooltip.text;
348
349      // If tooltip is already shown, just update the text.
350      if (tooltip.text && this.tooltipActive_ && !$('bubble').hidden) {
351        // If both previous and the new tooltip are supposed to be shown
352        // automatically, keep the autoshown flag.
353        var markAutoshown = this.tooltipAutoshown_ && tooltip.autoshow;
354        this.hideTooltip_(true);
355        this.showTooltip_();
356        this.tooltipAutoshown_ = markAutoshown;
357        return;
358      }
359
360      // If autoshow flag is set, make sure the tooltip gets shown.
361      if (tooltip.text && tooltip.autoshow) {
362        this.hideTooltip_(true);
363        this.showTooltip_();
364        this.tooltipAutoshown_ = true;
365        // Reset the tooltip active flag, which gets set in |showTooltip_|.
366        this.tooltipActive_ = false;
367        return;
368      }
369
370      this.hideTooltip_(true);
371    },
372
373    /**
374     * Sets up icon tabIndex attribute and handler for click and 'Enter' key
375     * down events.
376     * @param {?function()} callback If icon should be interactive, the
377     *     function to get called on click and 'Enter' key down events. Should
378     *     be null to make the icon  non interactive.
379     */
380    setInteractive: function(callback) {
381      this.iconElement.classList.toggle('interactive-custom-icon', !!callback);
382
383      // Update tabIndex property if needed.
384      if (!!this.actionHandler_ != !!callback) {
385        if (callback) {
386          this.iconElement.setAttribute('tabIndex',
387                                         UserPodTabOrder.POD_CUSTOM_ICON);
388        } else {
389          this.iconElement.removeAttribute('tabIndex');
390        }
391      }
392
393      // Set the new action handler.
394      this.actionHandler_ = callback;
395    },
396
397    /**
398     * Hides the icon and cleans its state.
399     */
400    hide: function() {
401      this.hideTooltip_(true);
402      this.hidden = true;
403      this.setInteractive(null);
404      this.resetHideTransitionState_();
405    },
406
407    /**
408     * Ensures the icon's transition state potentially set by a call to
409     * {@code fadeOut} is cleared.
410     * @private
411     */
412    resetHideTransitionState_: function() {
413      if (this.hideTransitionListener_) {
414        this.iconElement.removeEventListener('webkitTransitionEnd',
415                                             this.hideTransitionListener_);
416        this.hideTransitionListener_ = null;
417      }
418      this.iconElement.classList.toggle('faded', false);
419    },
420
421    /**
422     * Handles mouse down event in the icon element.
423     * @param {Event} e The mouse down event.
424     * @private
425     */
426    handleMouseDown_: function(e) {
427      this.hideTooltip_(false);
428      // Stop the event propagation so in the case the click ends up on the
429      // user pod (outside the custom icon) auth is not attempted.
430      stopEventPropagation(e);
431    },
432
433    /**
434     * Handles click event on the icon element. No-op if
435     * {@code this.actionHandler_} is not set.
436     * @param {Event} e The click event.
437     * @private
438     */
439    handleClick_: function(e) {
440      if (!this.actionHandler_)
441        return;
442      this.actionHandler_();
443      stopEventPropagation(e);
444    },
445
446    /**
447     * Handles key down event on the icon element. Only 'Enter' key is handled.
448     * No-op if {@code this.actionHandler_} is not set.
449     * @param {Event} e The key down event.
450     * @private
451     */
452    handleKeyDown_: function(e) {
453      if (!this.actionHandler_ || e.keyIdentifier != 'Enter')
454        return;
455      this.actionHandler_(e);
456      stopEventPropagation(e);
457    },
458
459    /**
460     * Called when mouse enters the icon. It sets timeout for showing the
461     * tooltip.
462     * @private
463     */
464    showTooltipSoon_: function() {
465      if (this.showTooltipTimeout_ || this.tooltipActive_)
466        return;
467      this.showTooltipTimeout_ =
468          setTimeout(this.showTooltip_.bind(this), 1000);
469    },
470
471    /**
472     * Shows the current tooltip if one is set.
473     * @private
474     */
475    showTooltip_: function() {
476      if (this.hidden || !this.tooltip_ || this.tooltipActive_)
477        return;
478
479      // If autoshown bubble got hidden, clear the autoshown flag.
480      if ($('bubble').hidden && this.tooltipAutoshown_)
481        this.tooltipAutoshown_ = false;
482
483      // Show the tooltip bubble.
484      var bubbleContent = document.createElement('div');
485      bubbleContent.textContent = this.tooltip_;
486
487      /** @const */ var BUBBLE_OFFSET = CUSTOM_ICON_CONTAINER_SIZE / 2;
488      // TODO(tengs): Introduce a special reauth state for the account picker,
489      // instead of showing the tooltip bubble here (crbug.com/409427).
490      /** @const */ var BUBBLE_PADDING = 8 + (this.iconId_ ? 0 : 23);
491      $('bubble').showContentForElement(this,
492                                        cr.ui.Bubble.Attachment.RIGHT,
493                                        bubbleContent,
494                                        BUBBLE_OFFSET,
495                                        BUBBLE_PADDING);
496      this.ensureTooltipTimeoutCleared_();
497      this.tooltipActive_ = true;
498    },
499
500    /**
501     * Hides the tooltip. If the current tooltip was automatically shown, it
502     * will be hidden only if |force| is set.
503     * @param {boolean} Whether the tooltip should be hidden if it got shown
504     *     because autoshow flag was set.
505     * @private
506     */
507    hideTooltip_: function(force) {
508      this.tooltipActive_ = false;
509      this.ensureTooltipTimeoutCleared_();
510      if (!force && this.tooltipAutoshown_)
511        return;
512      $('bubble').hideForElement(this);
513      this.tooltipAutoshown_ = false;
514    },
515
516    /**
517     * Clears timaout for showing the tooltip if it's set.
518     * @private
519     */
520    ensureTooltipTimeoutCleared_: function() {
521      if (this.showTooltipTimeout_) {
522        clearTimeout(this.showTooltipTimeout_);
523        this.showTooltipTimeout_ = null;
524      }
525    },
526  };
527
528  /**
529   * Unique salt added to user image URLs to prevent caching. Dictionary with
530   * user names as keys.
531   * @type {Object}
532   */
533  UserPod.userImageSalt_ = {};
534
535  UserPod.prototype = {
536    __proto__: HTMLDivElement.prototype,
537
538    /**
539     * Whether click on the pod can issue a user click auth attempt. The
540     * attempt can be issued iff the pod was focused when the click
541     * started (i.e. on mouse down event).
542     * @type {boolean}
543     * @private
544     */
545    userClickAuthAllowed_: false,
546
547    /** @override */
548    decorate: function() {
549      this.tabIndex = UserPodTabOrder.POD_INPUT;
550      this.actionBoxAreaElement.tabIndex = UserPodTabOrder.ACTION_BOX;
551
552      this.addEventListener('keydown', this.handlePodKeyDown_.bind(this));
553      this.addEventListener('click', this.handleClickOnPod_.bind(this));
554      this.addEventListener('mousedown', this.handlePodMouseDown_.bind(this));
555
556      this.signinButtonElement.addEventListener('click',
557          this.activate.bind(this));
558
559      this.actionBoxAreaElement.addEventListener('mousedown',
560                                                 stopEventPropagation);
561      this.actionBoxAreaElement.addEventListener('click',
562          this.handleActionAreaButtonClick_.bind(this));
563      this.actionBoxAreaElement.addEventListener('keydown',
564          this.handleActionAreaButtonKeyDown_.bind(this));
565
566      this.actionBoxMenuRemoveElement.addEventListener('click',
567          this.handleRemoveCommandClick_.bind(this));
568      this.actionBoxMenuRemoveElement.addEventListener('keydown',
569          this.handleRemoveCommandKeyDown_.bind(this));
570      this.actionBoxMenuRemoveElement.addEventListener('blur',
571          this.handleRemoveCommandBlur_.bind(this));
572      this.actionBoxRemoveUserWarningButtonElement.addEventListener(
573          'click',
574          this.handleRemoveUserConfirmationClick_.bind(this));
575        this.actionBoxRemoveUserWarningButtonElement.addEventListener(
576            'keydown',
577            this.handleRemoveUserConfirmationKeyDown_.bind(this));
578
579      var customIcon = this.customIconElement;
580      customIcon.parentNode.replaceChild(new UserPodCustomIcon(), customIcon);
581    },
582
583    /**
584     * Initializes the pod after its properties set and added to a pod row.
585     */
586    initialize: function() {
587      this.passwordElement.addEventListener('keydown',
588          this.parentNode.handleKeyDown.bind(this.parentNode));
589      this.passwordElement.addEventListener('keypress',
590          this.handlePasswordKeyPress_.bind(this));
591
592      this.imageElement.addEventListener('load',
593          this.parentNode.handlePodImageLoad.bind(this.parentNode, this));
594
595      var initialAuthType = this.user.initialAuthType ||
596          AUTH_TYPE.OFFLINE_PASSWORD;
597      this.setAuthType(initialAuthType, null);
598
599      this.userClickAuthAllowed_ = false;
600    },
601
602    /**
603     * Resets tab order for pod elements to its initial state.
604     */
605    resetTabOrder: function() {
606      // Note: the |mainInput| can be the pod itself.
607      this.mainInput.tabIndex = -1;
608      this.tabIndex = UserPodTabOrder.POD_INPUT;
609    },
610
611    /**
612     * Handles keypress event (i.e. any textual input) on password input.
613     * @param {Event} e Keypress Event object.
614     * @private
615     */
616    handlePasswordKeyPress_: function(e) {
617      // When tabbing from the system tray a tab key press is received. Suppress
618      // this so as not to type a tab character into the password field.
619      if (e.keyCode == 9) {
620        e.preventDefault();
621        return;
622      }
623    },
624
625    /**
626     * Top edge margin number of pixels.
627     * @type {?number}
628     */
629    set top(top) {
630      this.style.top = cr.ui.toCssPx(top);
631    },
632
633    /**
634     * Top edge margin number of pixels.
635     */
636    get top() {
637      return parseInt(this.style.top);
638    },
639
640    /**
641     * Left edge margin number of pixels.
642     * @type {?number}
643     */
644    set left(left) {
645      this.style.left = cr.ui.toCssPx(left);
646    },
647
648    /**
649     * Left edge margin number of pixels.
650     */
651    get left() {
652      return parseInt(this.style.left);
653    },
654
655    /**
656     * Height number of pixels.
657     */
658    get height() {
659      return this.offsetHeight;
660    },
661
662    /**
663     * Gets image element.
664     * @type {!HTMLImageElement}
665     */
666    get imageElement() {
667      return this.querySelector('.user-image');
668    },
669
670    /**
671     * Gets name element.
672     * @type {!HTMLDivElement}
673     */
674    get nameElement() {
675      return this.querySelector('.name');
676    },
677
678    /**
679     * Gets the container holding the password field.
680     * @type {!HTMLInputElement}
681     */
682    get passwordEntryContainerElement() {
683      return this.querySelector('.password-entry-container');
684    },
685
686    /**
687     * Gets password field.
688     * @type {!HTMLInputElement}
689     */
690    get passwordElement() {
691      return this.querySelector('.password');
692    },
693
694    /**
695     * Gets the password label, which is used to show a message where the
696     * password field is normally.
697     * @type {!HTMLInputElement}
698     */
699    get passwordLabelElement() {
700      return this.querySelector('.password-label');
701    },
702
703    /**
704     * Gets user sign in button.
705     * @type {!HTMLButtonElement}
706     */
707    get signinButtonElement() {
708      return this.querySelector('.signin-button');
709    },
710
711    /**
712     * Gets the container holding the launch app button.
713     * @type {!HTMLButtonElement}
714     */
715    get launchAppButtonContainerElement() {
716      return this.querySelector('.launch-app-button-container');
717    },
718
719    /**
720     * Gets launch app button.
721     * @type {!HTMLButtonElement}
722     */
723    get launchAppButtonElement() {
724      return this.querySelector('.launch-app-button');
725    },
726
727    /**
728     * Gets action box area.
729     * @type {!HTMLInputElement}
730     */
731    get actionBoxAreaElement() {
732      return this.querySelector('.action-box-area');
733    },
734
735    /**
736     * Gets user type icon area.
737     * @type {!HTMLDivElement}
738     */
739    get userTypeIconAreaElement() {
740      return this.querySelector('.user-type-icon-area');
741    },
742
743    /**
744     * Gets user type bubble like multi-profiles policy restriction message.
745     * @type {!HTMLDivElement}
746     */
747    get userTypeBubbleElement() {
748      return this.querySelector('.user-type-bubble');
749    },
750
751    /**
752     * Gets action box menu.
753     * @type {!HTMLInputElement}
754     */
755    get actionBoxMenu() {
756      return this.querySelector('.action-box-menu');
757    },
758
759    /**
760     * Gets action box menu title, user name item.
761     * @type {!HTMLInputElement}
762     */
763    get actionBoxMenuTitleNameElement() {
764      return this.querySelector('.action-box-menu-title-name');
765    },
766
767    /**
768     * Gets action box menu title, user email item.
769     * @type {!HTMLInputElement}
770     */
771    get actionBoxMenuTitleEmailElement() {
772      return this.querySelector('.action-box-menu-title-email');
773    },
774
775    /**
776     * Gets action box menu, remove user command item.
777     * @type {!HTMLInputElement}
778     */
779    get actionBoxMenuCommandElement() {
780      return this.querySelector('.action-box-menu-remove-command');
781    },
782
783    /**
784     * Gets action box menu, remove user command item div.
785     * @type {!HTMLInputElement}
786     */
787    get actionBoxMenuRemoveElement() {
788      return this.querySelector('.action-box-menu-remove');
789    },
790
791    /**
792     * Gets action box menu, remove user warning text div.
793     * @type {!HTMLInputElement}
794     */
795    get actionBoxRemoveUserWarningTextElement() {
796      return this.querySelector('.action-box-remove-user-warning-text');
797    },
798
799    /**
800     * Gets action box menu, remove supervised user warning text div.
801     * @type {!HTMLInputElement}
802     */
803    get actionBoxRemoveSupervisedUserWarningTextElement() {
804      return this.querySelector(
805          '.action-box-remove-supervised-user-warning-text');
806    },
807
808    /**
809     * Gets action box menu, remove user command item div.
810     * @type {!HTMLInputElement}
811     */
812    get actionBoxRemoveUserWarningElement() {
813      return this.querySelector('.action-box-remove-user-warning');
814    },
815
816    /**
817     * Gets action box menu, remove user command item div.
818     * @type {!HTMLInputElement}
819     */
820    get actionBoxRemoveUserWarningButtonElement() {
821      return this.querySelector('.remove-warning-button');
822    },
823
824    /**
825     * Gets the custom icon. This icon is normally hidden, but can be shown
826     * using the chrome.screenlockPrivate API.
827     * @type {!HTMLDivElement}
828     */
829    get customIconElement() {
830      return this.querySelector('.custom-icon-container');
831    },
832
833    /**
834     * Updates the user pod element.
835     */
836    update: function() {
837      this.imageElement.src = 'chrome://userimage/' + this.user.username +
838          '?id=' + UserPod.userImageSalt_[this.user.username];
839
840      this.nameElement.textContent = this.user_.displayName;
841      this.classList.toggle('signed-in', this.user_.signedIn);
842
843      if (this.isAuthTypeUserClick)
844        this.passwordLabelElement.textContent = this.authValue;
845
846      this.updateActionBoxArea();
847
848      this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF(
849        'passwordFieldAccessibleName', this.user_.emailAddress));
850
851      this.customizeUserPodPerUserType();
852    },
853
854    updateActionBoxArea: function() {
855      if (this.user_.publicAccount || this.user_.isApp) {
856        this.actionBoxAreaElement.hidden = true;
857        return;
858      }
859
860      this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
861
862      this.actionBoxAreaElement.setAttribute(
863          'aria-label', loadTimeData.getStringF(
864              'podMenuButtonAccessibleName', this.user_.emailAddress));
865      this.actionBoxMenuRemoveElement.setAttribute(
866          'aria-label', loadTimeData.getString(
867               'podMenuRemoveItemAccessibleName'));
868      this.actionBoxMenuTitleNameElement.textContent = this.user_.isOwner ?
869          loadTimeData.getStringF('ownerUserPattern', this.user_.displayName) :
870          this.user_.displayName;
871      this.actionBoxMenuTitleEmailElement.textContent = this.user_.emailAddress;
872      this.actionBoxMenuTitleEmailElement.hidden = this.user_.supervisedUser;
873
874      this.actionBoxMenuCommandElement.textContent =
875          loadTimeData.getString('removeUser');
876    },
877
878    customizeUserPodPerUserType: function() {
879      if (this.user_.supervisedUser && !this.user_.isDesktopUser) {
880        this.setUserPodIconType('supervised');
881      } else if (this.multiProfilesPolicyApplied) {
882        // Mark user pod as not focusable which in addition to the grayed out
883        // filter makes it look in disabled state.
884        this.classList.add('multiprofiles-policy-applied');
885        this.setUserPodIconType('policy');
886
887        if (this.user.multiProfilesPolicy == 'primary-only')
888          this.querySelector('.mp-policy-primary-only-msg').hidden = false;
889        else if (this.user.multiProfilesPolicy == 'owner-primary-only')
890          this.querySelector('.mp-owner-primary-only-msg').hidden = false;
891        else
892          this.querySelector('.mp-policy-not-allowed-msg').hidden = false;
893      } else if (this.user_.isApp) {
894        this.setUserPodIconType('app');
895      }
896    },
897
898    setUserPodIconType: function(userTypeClass) {
899      this.userTypeIconAreaElement.classList.add(userTypeClass);
900      this.userTypeIconAreaElement.hidden = false;
901    },
902
903    /**
904     * The user that this pod represents.
905     * @type {!Object}
906     */
907    user_: undefined,
908    get user() {
909      return this.user_;
910    },
911    set user(userDict) {
912      this.user_ = userDict;
913      this.update();
914    },
915
916    /**
917     * Returns true if multi-profiles sign in is currently active and this
918     * user pod is restricted per policy.
919     * @type {boolean}
920     */
921    get multiProfilesPolicyApplied() {
922      var isMultiProfilesUI =
923        (Oobe.getInstance().displayType == DISPLAY_TYPE.USER_ADDING);
924      return isMultiProfilesUI && !this.user_.isMultiProfilesAllowed;
925    },
926
927    /**
928     * Gets main input element.
929     * @type {(HTMLButtonElement|HTMLInputElement)}
930     */
931    get mainInput() {
932      if (this.isAuthTypePassword) {
933        return this.passwordElement;
934      } else if (this.isAuthTypeOnlineSignIn) {
935        return this.signinButtonElement;
936      } else if (this.isAuthTypeUserClick) {
937        return this.passwordLabelElement;
938      }
939    },
940
941    /**
942     * Whether action box button is in active state.
943     * @type {boolean}
944     */
945    get isActionBoxMenuActive() {
946      return this.actionBoxAreaElement.classList.contains('active');
947    },
948    set isActionBoxMenuActive(active) {
949      if (active == this.isActionBoxMenuActive)
950        return;
951
952      if (active) {
953        this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
954        this.actionBoxRemoveUserWarningElement.hidden = true;
955
956        // Clear focus first if another pod is focused.
957        if (!this.parentNode.isFocused(this)) {
958          this.parentNode.focusPod(undefined, true);
959          this.actionBoxAreaElement.focus();
960        }
961
962        // Hide user-type-bubble.
963        this.userTypeBubbleElement.classList.remove('bubble-shown');
964
965        this.actionBoxAreaElement.classList.add('active');
966
967        // If the user pod is on either edge of the screen, then the menu
968        // could be displayed partially ofscreen.
969        this.actionBoxMenu.classList.remove('left-edge-offset');
970        this.actionBoxMenu.classList.remove('right-edge-offset');
971
972        var offsetLeft =
973            cr.ui.login.DisplayManager.getOffset(this.actionBoxMenu).left;
974        var menuWidth = this.actionBoxMenu.offsetWidth;
975        if (offsetLeft < 0)
976          this.actionBoxMenu.classList.add('left-edge-offset');
977        else if (offsetLeft + menuWidth > window.innerWidth)
978          this.actionBoxMenu.classList.add('right-edge-offset');
979      } else {
980        this.actionBoxAreaElement.classList.remove('active');
981        this.actionBoxAreaElement.classList.remove('menu-moved-up');
982        this.actionBoxMenu.classList.remove('menu-moved-up');
983      }
984    },
985
986    /**
987     * Whether action box button is in hovered state.
988     * @type {boolean}
989     */
990    get isActionBoxMenuHovered() {
991      return this.actionBoxAreaElement.classList.contains('hovered');
992    },
993    set isActionBoxMenuHovered(hovered) {
994      if (hovered == this.isActionBoxMenuHovered)
995        return;
996
997      if (hovered) {
998        this.actionBoxAreaElement.classList.add('hovered');
999        this.classList.add('hovered');
1000      } else {
1001        if (this.multiProfilesPolicyApplied)
1002          this.userTypeBubbleElement.classList.remove('bubble-shown');
1003        this.actionBoxAreaElement.classList.remove('hovered');
1004        this.classList.remove('hovered');
1005      }
1006    },
1007
1008    /**
1009     * Set the authentication type for the pod.
1010     * @param {number} An auth type value defined in the AUTH_TYPE enum.
1011     * @param {string} authValue The initial value used for the auth type.
1012     */
1013    setAuthType: function(authType, authValue) {
1014      this.authType_ = authType;
1015      this.authValue_ = authValue;
1016      this.setAttribute('auth-type', AUTH_TYPE_NAMES[this.authType_]);
1017      this.update();
1018      this.reset(this.parentNode.isFocused(this));
1019    },
1020
1021    /**
1022     * The auth type of the user pod. This value is one of the enum
1023     * values in AUTH_TYPE.
1024     * @type {number}
1025     */
1026    get authType() {
1027      return this.authType_;
1028    },
1029
1030    /**
1031     * The initial value used for the pod's authentication type.
1032     * eg. a prepopulated password input when using password authentication.
1033     */
1034    get authValue() {
1035      return this.authValue_;
1036    },
1037
1038    /**
1039     * True if the the user pod uses a password to authenticate.
1040     * @type {bool}
1041     */
1042    get isAuthTypePassword() {
1043      return this.authType_ == AUTH_TYPE.OFFLINE_PASSWORD ||
1044             this.authType_ == AUTH_TYPE.FORCE_OFFLINE_PASSWORD;
1045    },
1046
1047    /**
1048     * True if the the user pod uses a user click to authenticate.
1049     * @type {bool}
1050     */
1051    get isAuthTypeUserClick() {
1052      return this.authType_ == AUTH_TYPE.USER_CLICK;
1053    },
1054
1055    /**
1056     * True if the the user pod uses a online sign in to authenticate.
1057     * @type {bool}
1058     */
1059    get isAuthTypeOnlineSignIn() {
1060      return this.authType_ == AUTH_TYPE.ONLINE_SIGN_IN;
1061    },
1062
1063    /**
1064     * Updates the image element of the user.
1065     */
1066    updateUserImage: function() {
1067      UserPod.userImageSalt_[this.user.username] = new Date().getTime();
1068      this.update();
1069    },
1070
1071    /**
1072     * Focuses on input element.
1073     */
1074    focusInput: function() {
1075      // Move tabIndex from the whole pod to the main input.
1076      // Note: the |mainInput| can be the pod itself.
1077      this.tabIndex = -1;
1078      this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1079      this.mainInput.focus();
1080    },
1081
1082    /**
1083     * Activates the pod.
1084     * @param {Event} e Event object.
1085     * @return {boolean} True if activated successfully.
1086     */
1087    activate: function(e) {
1088      if (this.isAuthTypeOnlineSignIn) {
1089        this.showSigninUI();
1090      } else if (this.isAuthTypeUserClick) {
1091        Oobe.disableSigninUI();
1092        this.classList.toggle('signing-in', true);
1093        chrome.send('attemptUnlock', [this.user.username]);
1094      } else if (this.isAuthTypePassword) {
1095        if (!this.passwordElement.value)
1096          return false;
1097        Oobe.disableSigninUI();
1098        chrome.send('authenticateUser',
1099                    [this.user.username, this.passwordElement.value]);
1100      } else {
1101        console.error('Activating user pod with invalid authentication type: ' +
1102            this.authType);
1103      }
1104
1105      return true;
1106    },
1107
1108    showSupervisedUserSigninWarning: function() {
1109      // Supervised user token has been invalidated.
1110      // Make sure that pod is focused i.e. "Sign in" button is seen.
1111      this.parentNode.focusPod(this);
1112
1113      var error = document.createElement('div');
1114      var messageDiv = document.createElement('div');
1115      messageDiv.className = 'error-message-bubble';
1116      messageDiv.textContent =
1117          loadTimeData.getString('supervisedUserExpiredTokenWarning');
1118      error.appendChild(messageDiv);
1119
1120      $('bubble').showContentForElement(
1121          this.signinButtonElement,
1122          cr.ui.Bubble.Attachment.TOP,
1123          error,
1124          this.signinButtonElement.offsetWidth / 2,
1125          4);
1126      // Move warning bubble up if it overlaps the shelf.
1127      var maxHeight =
1128          cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping($('bubble'));
1129      if (maxHeight < $('bubble').offsetHeight) {
1130        $('bubble').showContentForElement(
1131            this.signinButtonElement,
1132            cr.ui.Bubble.Attachment.BOTTOM,
1133            error,
1134            this.signinButtonElement.offsetWidth / 2,
1135            4);
1136      }
1137    },
1138
1139    /**
1140     * Shows signin UI for this user.
1141     */
1142    showSigninUI: function() {
1143      if (this.user.supervisedUser && !this.user.isDesktopUser) {
1144        this.showSupervisedUserSigninWarning();
1145      } else {
1146        // Special case for multi-profiles sign in. We show users even if they
1147        // are not allowed per policy. Restrict those users from starting GAIA.
1148        if (this.multiProfilesPolicyApplied)
1149          return;
1150
1151        this.parentNode.showSigninUI(this.user.emailAddress);
1152      }
1153    },
1154
1155    /**
1156     * Resets the input field and updates the tab order of pod controls.
1157     * @param {boolean} takeFocus If true, input field takes focus.
1158     */
1159    reset: function(takeFocus) {
1160      this.passwordElement.value = '';
1161      this.classList.toggle('signing-in', false);
1162      if (takeFocus) {
1163        if (!this.multiProfilesPolicyApplied)
1164          this.focusInput();  // This will set a custom tab order.
1165      }
1166      else
1167        this.resetTabOrder();
1168    },
1169
1170    /**
1171     * Removes a user using the correct identifier based on user type.
1172     * @param {Object} user User to be removed.
1173     */
1174    removeUser: function(user) {
1175      chrome.send('removeUser',
1176                  [user.isDesktopUser ? user.profilePath : user.username]);
1177    },
1178
1179    /**
1180     * Handles a click event on action area button.
1181     * @param {Event} e Click event.
1182     */
1183    handleActionAreaButtonClick_: function(e) {
1184      if (this.parentNode.disabled)
1185        return;
1186      this.isActionBoxMenuActive = !this.isActionBoxMenuActive;
1187      e.stopPropagation();
1188    },
1189
1190    /**
1191     * Handles a keydown event on action area button.
1192     * @param {Event} e KeyDown event.
1193     */
1194    handleActionAreaButtonKeyDown_: function(e) {
1195      if (this.disabled)
1196        return;
1197      switch (e.keyIdentifier) {
1198        case 'Enter':
1199        case 'U+0020':  // Space
1200          if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive)
1201            this.isActionBoxMenuActive = true;
1202          e.stopPropagation();
1203          break;
1204        case 'Up':
1205        case 'Down':
1206          if (this.isActionBoxMenuActive) {
1207            this.actionBoxMenuRemoveElement.tabIndex =
1208                UserPodTabOrder.PAD_MENU_ITEM;
1209            this.actionBoxMenuRemoveElement.focus();
1210          }
1211          e.stopPropagation();
1212          break;
1213        case 'U+001B':  // Esc
1214          this.isActionBoxMenuActive = false;
1215          e.stopPropagation();
1216          break;
1217        case 'U+0009':  // Tab
1218          if (!this.parentNode.alwaysFocusSinglePod)
1219            this.parentNode.focusPod();
1220        default:
1221          this.isActionBoxMenuActive = false;
1222          break;
1223      }
1224    },
1225
1226    /**
1227     * Handles a click event on remove user command.
1228     * @param {Event} e Click event.
1229     */
1230    handleRemoveCommandClick_: function(e) {
1231      if (this.user.supervisedUser || this.user.isDesktopUser) {
1232        this.showRemoveWarning_();
1233        return;
1234      }
1235      if (this.isActionBoxMenuActive)
1236        chrome.send('removeUser', [this.user.username]);
1237    },
1238
1239    /**
1240     * Shows remove user warning. Used for supervised users on CrOS, and for all
1241     * users on desktop.
1242     */
1243    showRemoveWarning_: function() {
1244      this.actionBoxMenuRemoveElement.hidden = true;
1245      this.actionBoxRemoveUserWarningElement.hidden = false;
1246      this.actionBoxRemoveUserWarningButtonElement.focus();
1247
1248      // Move up the menu if it overlaps shelf.
1249      var maxHeight = cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping(
1250          this.actionBoxMenu);
1251      var actualHeight = parseInt(
1252          window.getComputedStyle(this.actionBoxMenu).height);
1253      if (maxHeight < actualHeight) {
1254        this.actionBoxMenu.classList.add('menu-moved-up');
1255        this.actionBoxAreaElement.classList.add('menu-moved-up');
1256      }
1257    },
1258
1259    /**
1260     * Handles a click event on remove user confirmation button.
1261     * @param {Event} e Click event.
1262     */
1263    handleRemoveUserConfirmationClick_: function(e) {
1264      if (this.isActionBoxMenuActive) {
1265        this.isActionBoxMenuActive = false;
1266        this.removeUser(this.user);
1267        e.stopPropagation();
1268      }
1269    },
1270
1271    /**
1272     * Handles a keydown event on remove user confirmation button.
1273     * @param {Event} e KeyDown event.
1274     */
1275    handleRemoveUserConfirmationKeyDown_: function(e) {
1276      if (!this.isActionBoxMenuActive)
1277        return;
1278
1279      // Only handle pressing 'Enter' or 'Space', and let all other events
1280      // bubble to the action box menu.
1281      if (e.keyIdentifier == 'Enter' || e.keyIdentifier == 'U+0020') {
1282        this.isActionBoxMenuActive = false;
1283        this.removeUser(this.user);
1284        e.stopPropagation();
1285        // Prevent default so that we don't trigger a 'click' event.
1286        e.preventDefault();
1287      }
1288    },
1289
1290    /**
1291     * Handles a keydown event on remove command.
1292     * @param {Event} e KeyDown event.
1293     */
1294    handleRemoveCommandKeyDown_: function(e) {
1295      if (this.disabled)
1296        return;
1297      switch (e.keyIdentifier) {
1298        case 'Enter':
1299          if (this.user.supervisedUser || this.user.isDesktopUser) {
1300            // Prevent default so that we don't trigger a 'click' event on the
1301            // remove button that will be focused.
1302            e.preventDefault();
1303            this.showRemoveWarning_();
1304          } else {
1305            this.removeUser(this.user);
1306          }
1307          e.stopPropagation();
1308          break;
1309        case 'Up':
1310        case 'Down':
1311          e.stopPropagation();
1312          break;
1313        case 'U+001B':  // Esc
1314          this.actionBoxAreaElement.focus();
1315          this.isActionBoxMenuActive = false;
1316          e.stopPropagation();
1317          break;
1318        default:
1319          this.actionBoxAreaElement.focus();
1320          this.isActionBoxMenuActive = false;
1321          break;
1322      }
1323    },
1324
1325    /**
1326     * Handles a blur event on remove command.
1327     * @param {Event} e Blur event.
1328     */
1329    handleRemoveCommandBlur_: function(e) {
1330      if (this.disabled)
1331        return;
1332      this.actionBoxMenuRemoveElement.tabIndex = -1;
1333    },
1334
1335    /**
1336     * Handles mouse down event. It sets whether the user click auth will be
1337     * allowed on the next mouse click event. The auth is allowed iff the pod
1338     * was focused on the mouse down event starting the click.
1339     * @param {Event} e The mouse down event.
1340     */
1341    handlePodMouseDown_: function(e) {
1342      this.userClickAuthAllowed_ = this.parentNode.isFocused(this);
1343    },
1344
1345    /**
1346     * Handles click event on a user pod.
1347     * @param {Event} e Click event.
1348     */
1349    handleClickOnPod_: function(e) {
1350      if (this.parentNode.disabled)
1351        return;
1352
1353      if (!this.isActionBoxMenuActive) {
1354        if (this.isAuthTypeOnlineSignIn) {
1355          this.showSigninUI();
1356        } else if (this.isAuthTypeUserClick && this.userClickAuthAllowed_) {
1357          // Note that this.userClickAuthAllowed_ is set in mouse down event
1358          // handler.
1359          this.parentNode.setActivatedPod(this);
1360        }
1361
1362        if (this.multiProfilesPolicyApplied)
1363          this.userTypeBubbleElement.classList.add('bubble-shown');
1364
1365        // Prevent default so that we don't trigger 'focus' event.
1366        e.preventDefault();
1367      }
1368    },
1369
1370    /**
1371     * Handles keydown event for a user pod.
1372     * @param {Event} e Key event.
1373     */
1374    handlePodKeyDown_: function(e) {
1375      if (!this.isAuthTypeUserClick || this.disabled)
1376        return;
1377      switch (e.keyIdentifier) {
1378        case 'Enter':
1379        case 'U+0020':  // Space
1380          if (this.parentNode.isFocused(this))
1381            this.parentNode.setActivatedPod(this);
1382          break;
1383      }
1384    }
1385  };
1386
1387  /**
1388   * Creates a public account user pod.
1389   * @constructor
1390   * @extends {UserPod}
1391   */
1392  var PublicAccountUserPod = cr.ui.define(function() {
1393    var node = UserPod();
1394
1395    var extras = $('public-account-user-pod-extras-template').children;
1396    for (var i = 0; i < extras.length; ++i) {
1397      var el = extras[i].cloneNode(true);
1398      node.appendChild(el);
1399    }
1400
1401    return node;
1402  });
1403
1404  PublicAccountUserPod.prototype = {
1405    __proto__: UserPod.prototype,
1406
1407    /**
1408     * "Enter" button in expanded side pane.
1409     * @type {!HTMLButtonElement}
1410     */
1411    get enterButtonElement() {
1412      return this.querySelector('.enter-button');
1413    },
1414
1415    /**
1416     * Boolean flag of whether the pod is showing the side pane. The flag
1417     * controls whether 'expanded' class is added to the pod's class list and
1418     * resets tab order because main input element changes when the 'expanded'
1419     * state changes.
1420     * @type {boolean}
1421     */
1422    get expanded() {
1423      return this.classList.contains('expanded');
1424    },
1425
1426    set expanded(expanded) {
1427      if (this.expanded == expanded)
1428        return;
1429
1430      this.resetTabOrder();
1431      this.classList.toggle('expanded', expanded);
1432      if (expanded) {
1433        // Show the advanced expanded pod directly if there are at least two
1434        // recommended locales. This will be the case in multilingual
1435        // environments where users are likely to want to choose among locales.
1436        if (this.querySelector('.language-select').multipleRecommendedLocales)
1437          this.classList.add('advanced');
1438        this.usualLeft = this.left;
1439        this.makeSpaceForExpandedPod_();
1440      } else if (typeof(this.usualLeft) != 'undefined') {
1441        this.left = this.usualLeft;
1442      }
1443
1444      var self = this;
1445      this.classList.add('animating');
1446      this.addEventListener('webkitTransitionEnd', function f(e) {
1447        self.removeEventListener('webkitTransitionEnd', f);
1448        self.classList.remove('animating');
1449
1450        // Accessibility focus indicator does not move with the focused
1451        // element. Sends a 'focus' event on the currently focused element
1452        // so that accessibility focus indicator updates its location.
1453        if (document.activeElement)
1454          document.activeElement.dispatchEvent(new Event('focus'));
1455      });
1456      // Guard timer set to animation duration + 20ms.
1457      ensureTransitionEndEvent(this, 200);
1458    },
1459
1460    get advanced() {
1461      return this.classList.contains('advanced');
1462    },
1463
1464    /** @override */
1465    get mainInput() {
1466      if (this.expanded)
1467        return this.enterButtonElement;
1468      else
1469        return this.nameElement;
1470    },
1471
1472    /** @override */
1473    decorate: function() {
1474      UserPod.prototype.decorate.call(this);
1475
1476      this.classList.add('public-account');
1477
1478      this.nameElement.addEventListener('keydown', (function(e) {
1479        if (e.keyIdentifier == 'Enter') {
1480          this.parentNode.setActivatedPod(this, e);
1481          // Stop this keydown event from bubbling up to PodRow handler.
1482          e.stopPropagation();
1483          // Prevent default so that we don't trigger a 'click' event on the
1484          // newly focused "Enter" button.
1485          e.preventDefault();
1486        }
1487      }).bind(this));
1488
1489      var learnMore = this.querySelector('.learn-more');
1490      learnMore.addEventListener('mousedown', stopEventPropagation);
1491      learnMore.addEventListener('click', this.handleLearnMoreEvent);
1492      learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
1493
1494      learnMore = this.querySelector('.expanded-pane-learn-more');
1495      learnMore.addEventListener('click', this.handleLearnMoreEvent);
1496      learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
1497
1498      var languageSelect = this.querySelector('.language-select');
1499      languageSelect.tabIndex = UserPodTabOrder.POD_INPUT;
1500      languageSelect.manuallyChanged = false;
1501      languageSelect.addEventListener(
1502          'change',
1503          function() {
1504            languageSelect.manuallyChanged = true;
1505            this.getPublicSessionKeyboardLayouts_();
1506          }.bind(this));
1507
1508      var keyboardSelect = this.querySelector('.keyboard-select');
1509      keyboardSelect.tabIndex = UserPodTabOrder.POD_INPUT;
1510      keyboardSelect.loadedLocale = null;
1511
1512      var languageAndInput = this.querySelector('.language-and-input');
1513      languageAndInput.tabIndex = UserPodTabOrder.POD_INPUT;
1514      languageAndInput.addEventListener('click',
1515                                        this.transitionToAdvanced_.bind(this));
1516
1517      this.enterButtonElement.addEventListener('click', (function(e) {
1518        this.enterButtonElement.disabled = true;
1519        var locale = this.querySelector('.language-select').value;
1520        var keyboardSelect = this.querySelector('.keyboard-select');
1521        // The contents of |keyboardSelect| is updated asynchronously. If its
1522        // locale does not match |locale|, it has not updated yet and the
1523        // currently selected keyboard layout may not be applicable to |locale|.
1524        // Do not return any keyboard layout in this case and let the backend
1525        // choose a suitable layout.
1526        var keyboardLayout =
1527            keyboardSelect.loadedLocale == locale ? keyboardSelect.value : '';
1528        chrome.send('launchPublicSession',
1529                    [this.user.username, locale, keyboardLayout]);
1530      }).bind(this));
1531    },
1532
1533    /** @override **/
1534    initialize: function() {
1535      UserPod.prototype.initialize.call(this);
1536
1537      id = this.user.username + '-keyboard';
1538      this.querySelector('.keyboard-select-label').htmlFor = id;
1539      this.querySelector('.keyboard-select').setAttribute('id', id);
1540
1541      var id = this.user.username + '-language';
1542      this.querySelector('.language-select-label').htmlFor = id;
1543      var languageSelect = this.querySelector('.language-select');
1544      languageSelect.setAttribute('id', id);
1545      this.populateLanguageSelect(this.user.initialLocales,
1546                                  this.user.initialLocale,
1547                                  this.user.initialMultipleRecommendedLocales);
1548    },
1549
1550    /** @override **/
1551    update: function() {
1552      UserPod.prototype.update.call(this);
1553      this.querySelector('.expanded-pane-name').textContent =
1554          this.user_.displayName;
1555      this.querySelector('.info').textContent =
1556          loadTimeData.getStringF('publicAccountInfoFormat',
1557                                  this.user_.enterpriseDomain);
1558    },
1559
1560    /** @override */
1561    focusInput: function() {
1562      // Move tabIndex from the whole pod to the main input.
1563      this.tabIndex = -1;
1564      this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1565      this.mainInput.focus();
1566    },
1567
1568    /** @override */
1569    reset: function(takeFocus) {
1570      if (!takeFocus)
1571        this.expanded = false;
1572      this.enterButtonElement.disabled = false;
1573      UserPod.prototype.reset.call(this, takeFocus);
1574    },
1575
1576    /** @override */
1577    activate: function(e) {
1578      if (!this.expanded) {
1579        this.expanded = true;
1580        this.focusInput();
1581      }
1582      return true;
1583    },
1584
1585    /** @override */
1586    handleClickOnPod_: function(e) {
1587      if (this.parentNode.disabled)
1588        return;
1589
1590      this.parentNode.focusPod(this);
1591      this.parentNode.setActivatedPod(this, e);
1592      // Prevent default so that we don't trigger 'focus' event.
1593      e.preventDefault();
1594    },
1595
1596    /**
1597     * Updates the display name shown on the pod.
1598     * @param {string} displayName The new display name
1599     */
1600    setDisplayName: function(displayName) {
1601      this.user_.displayName = displayName;
1602      this.update();
1603    },
1604
1605    /**
1606     * Handle mouse and keyboard events for the learn more button. Triggering
1607     * the button causes information about public sessions to be shown.
1608     * @param {Event} event Mouse or keyboard event.
1609     */
1610    handleLearnMoreEvent: function(event) {
1611      switch (event.type) {
1612        // Show informaton on left click. Let any other clicks propagate.
1613        case 'click':
1614          if (event.button != 0)
1615            return;
1616          break;
1617        // Show informaton when <Return> or <Space> is pressed. Let any other
1618        // key presses propagate.
1619        case 'keydown':
1620          switch (event.keyCode) {
1621            case 13:  // Return.
1622            case 32:  // Space.
1623              break;
1624            default:
1625              return;
1626          }
1627          break;
1628      }
1629      chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]);
1630      stopEventPropagation(event);
1631    },
1632
1633    makeSpaceForExpandedPod_: function() {
1634      var width = this.classList.contains('advanced') ?
1635          PUBLIC_EXPANDED_ADVANCED_WIDTH : PUBLIC_EXPANDED_BASIC_WIDTH;
1636      var isDesktopUserManager = Oobe.getInstance().displayType ==
1637          DISPLAY_TYPE.DESKTOP_USER_MANAGER;
1638      var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
1639                                              POD_ROW_PADDING;
1640      if (this.left + width > $('pod-row').offsetWidth - rowPadding)
1641        this.left = $('pod-row').offsetWidth - rowPadding - width;
1642    },
1643
1644    /**
1645     * Transition the expanded pod from the basic to the advanced view.
1646     */
1647    transitionToAdvanced_: function() {
1648      var pod = this;
1649      var languageAndInputSection =
1650          this.querySelector('.language-and-input-section');
1651      this.classList.add('transitioning-to-advanced');
1652      setTimeout(function() {
1653        pod.classList.add('advanced');
1654        pod.makeSpaceForExpandedPod_();
1655        languageAndInputSection.addEventListener('webkitTransitionEnd',
1656                                                 function observer() {
1657          languageAndInputSection.removeEventListener('webkitTransitionEnd',
1658                                                      observer);
1659          pod.classList.remove('transitioning-to-advanced');
1660          pod.querySelector('.language-select').focus();
1661        });
1662        // Guard timer set to animation duration + 20ms.
1663        ensureTransitionEndEvent(languageAndInputSection, 380);
1664      }, 0);
1665    },
1666
1667    /**
1668     * Retrieves the list of keyboard layouts available for the currently
1669     * selected locale.
1670     */
1671    getPublicSessionKeyboardLayouts_: function() {
1672      var selectedLocale = this.querySelector('.language-select').value;
1673      if (selectedLocale ==
1674          this.querySelector('.keyboard-select').loadedLocale) {
1675        // If the list of keyboard layouts was loaded for the currently selected
1676        // locale, it is already up to date.
1677        return;
1678      }
1679      chrome.send('getPublicSessionKeyboardLayouts',
1680                  [this.user.username, selectedLocale]);
1681     },
1682
1683    /**
1684     * Populates the keyboard layout "select" element with a list of layouts.
1685     * @param {string} locale The locale to which this list of keyboard layouts
1686     *     applies
1687     * @param {!Object} list List of available keyboard layouts
1688     */
1689    populateKeyboardSelect: function(locale, list) {
1690      if (locale != this.querySelector('.language-select').value) {
1691        // The selected locale has changed and the list of keyboard layouts is
1692        // not applicable. This method will be called again when a list of
1693        // keyboard layouts applicable to the selected locale is retrieved.
1694        return;
1695      }
1696
1697      var keyboardSelect = this.querySelector('.keyboard-select');
1698      keyboardSelect.loadedLocale = locale;
1699      keyboardSelect.innerHTML = '';
1700      for (var i = 0; i < list.length; ++i) {
1701        var item = list[i];
1702        keyboardSelect.appendChild(
1703            new Option(item.title, item.value, item.selected, item.selected));
1704      }
1705    },
1706
1707    /**
1708     * Populates the language "select" element with a list of locales.
1709     * @param {!Object} locales The list of available locales
1710     * @param {string} defaultLocale The locale to select by default
1711     * @param {boolean} multipleRecommendedLocales Whether |locales| contains
1712     *     two or more recommended locales
1713     */
1714    populateLanguageSelect: function(locales,
1715                                     defaultLocale,
1716                                     multipleRecommendedLocales) {
1717      var languageSelect = this.querySelector('.language-select');
1718      // If the user manually selected a locale, do not change the selection.
1719      // Otherwise, select the new |defaultLocale|.
1720      var selected =
1721          languageSelect.manuallyChanged ? languageSelect.value : defaultLocale;
1722      languageSelect.innerHTML = '';
1723      var group = languageSelect;
1724      for (var i = 0; i < locales.length; ++i) {
1725        var item = locales[i];
1726        if (item.optionGroupName) {
1727          group = document.createElement('optgroup');
1728          group.label = item.optionGroupName;
1729          languageSelect.appendChild(group);
1730        } else {
1731          group.appendChild(new Option(item.title,
1732                                       item.value,
1733                                       item.value == selected,
1734                                       item.value == selected));
1735        }
1736      }
1737      languageSelect.multipleRecommendedLocales = multipleRecommendedLocales;
1738
1739      // Retrieve a list of keyboard layouts applicable to the locale that is
1740      // now selected.
1741      this.getPublicSessionKeyboardLayouts_();
1742    }
1743  };
1744
1745  /**
1746   * Creates a user pod to be used only in desktop chrome.
1747   * @constructor
1748   * @extends {UserPod}
1749   */
1750  var DesktopUserPod = cr.ui.define(function() {
1751    // Don't just instantiate a UserPod(), as this will call decorate() on the
1752    // parent object, and add duplicate event listeners.
1753    var node = $('user-pod-template').cloneNode(true);
1754    node.removeAttribute('id');
1755    return node;
1756  });
1757
1758  DesktopUserPod.prototype = {
1759    __proto__: UserPod.prototype,
1760
1761    /** @override */
1762    get mainInput() {
1763      if (this.user.needsSignin)
1764        return this.passwordElement;
1765      else
1766        return this.nameElement;
1767    },
1768
1769    /** @override */
1770    update: function() {
1771      this.imageElement.src = this.user.userImage;
1772      this.nameElement.textContent = this.user.displayName;
1773
1774      var isLockedUser = this.user.needsSignin;
1775      var isSupervisedUser = this.user.supervisedUser;
1776      this.classList.toggle('locked', isLockedUser);
1777      this.classList.toggle('supervised-user', isSupervisedUser);
1778
1779      if (this.isAuthTypeUserClick)
1780        this.passwordLabelElement.textContent = this.authValue;
1781
1782      this.actionBoxRemoveUserWarningTextElement.hidden = isSupervisedUser;
1783      this.actionBoxRemoveSupervisedUserWarningTextElement.hidden =
1784          !isSupervisedUser;
1785
1786      UserPod.prototype.updateActionBoxArea.call(this);
1787    },
1788
1789    /** @override */
1790    focusInput: function() {
1791      // Move tabIndex from the whole pod to the main input.
1792      this.tabIndex = -1;
1793      this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1794      this.mainInput.focus();
1795    },
1796
1797    /** @override */
1798    activate: function(e) {
1799      if (!this.user.needsSignin) {
1800        Oobe.launchUser(this.user.emailAddress, this.user.displayName);
1801      } else if (!this.passwordElement.value) {
1802        return false;
1803      } else {
1804        chrome.send('authenticatedLaunchUser',
1805                    [this.user.emailAddress,
1806                     this.user.displayName,
1807                     this.passwordElement.value]);
1808      }
1809      this.passwordElement.value = '';
1810      return true;
1811    },
1812
1813    /** @override */
1814    handleClickOnPod_: function(e) {
1815      if (this.parentNode.disabled)
1816        return;
1817
1818      Oobe.clearErrors();
1819      this.parentNode.lastFocusedPod_ = this;
1820
1821      // If this is an unlocked pod, then open a browser window. Otherwise
1822      // just activate the pod and show the password field.
1823      if (!this.user.needsSignin && !this.isActionBoxMenuActive)
1824        this.activate(e);
1825
1826      if (this.isAuthTypeUserClick)
1827        chrome.send('attemptUnlock', [this.user.emailAddress]);
1828    },
1829  };
1830
1831  /**
1832   * Creates a user pod that represents kiosk app.
1833   * @constructor
1834   * @extends {UserPod}
1835   */
1836  var KioskAppPod = cr.ui.define(function() {
1837    var node = UserPod();
1838    return node;
1839  });
1840
1841  KioskAppPod.prototype = {
1842    __proto__: UserPod.prototype,
1843
1844    /** @override */
1845    decorate: function() {
1846      UserPod.prototype.decorate.call(this);
1847      this.launchAppButtonElement.addEventListener('click',
1848                                                   this.activate.bind(this));
1849    },
1850
1851    /** @override */
1852    update: function() {
1853      this.imageElement.src = this.user.iconUrl;
1854      this.imageElement.alt = this.user.label;
1855      this.imageElement.title = this.user.label;
1856      this.passwordEntryContainerElement.hidden = true;
1857      this.launchAppButtonContainerElement.hidden = false;
1858      this.nameElement.textContent = this.user.label;
1859
1860      UserPod.prototype.updateActionBoxArea.call(this);
1861      UserPod.prototype.customizeUserPodPerUserType.call(this);
1862    },
1863
1864    /** @override */
1865    get mainInput() {
1866      return this.launchAppButtonElement;
1867    },
1868
1869    /** @override */
1870    focusInput: function() {
1871      // Move tabIndex from the whole pod to the main input.
1872      this.tabIndex = -1;
1873      this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1874      this.mainInput.focus();
1875    },
1876
1877    /** @override */
1878    get forceOnlineSignin() {
1879      return false;
1880    },
1881
1882    /** @override */
1883    activate: function(e) {
1884      var diagnosticMode = e && e.ctrlKey;
1885      this.launchApp_(this.user, diagnosticMode);
1886      return true;
1887    },
1888
1889    /** @override */
1890    handleClickOnPod_: function(e) {
1891      if (this.parentNode.disabled)
1892        return;
1893
1894      Oobe.clearErrors();
1895      this.parentNode.lastFocusedPod_ = this;
1896      this.activate(e);
1897    },
1898
1899    /**
1900     * Launch the app. If |diagnosticMode| is true, ask user to confirm.
1901     * @param {Object} app App data.
1902     * @param {boolean} diagnosticMode Whether to run the app in diagnostic
1903     *     mode.
1904     */
1905    launchApp_: function(app, diagnosticMode) {
1906      if (!diagnosticMode) {
1907        chrome.send('launchKioskApp', [app.id, false]);
1908        return;
1909      }
1910
1911      var oobe = $('oobe');
1912      if (!oobe.confirmDiagnosticMode_) {
1913        oobe.confirmDiagnosticMode_ =
1914            new cr.ui.dialogs.ConfirmDialog(document.body);
1915        oobe.confirmDiagnosticMode_.setOkLabel(
1916            loadTimeData.getString('confirmKioskAppDiagnosticModeYes'));
1917        oobe.confirmDiagnosticMode_.setCancelLabel(
1918            loadTimeData.getString('confirmKioskAppDiagnosticModeNo'));
1919      }
1920
1921      oobe.confirmDiagnosticMode_.show(
1922          loadTimeData.getStringF('confirmKioskAppDiagnosticModeFormat',
1923                                  app.label),
1924          function() {
1925            chrome.send('launchKioskApp', [app.id, true]);
1926          });
1927    },
1928  };
1929
1930  /**
1931   * Creates a new pod row element.
1932   * @constructor
1933   * @extends {HTMLDivElement}
1934   */
1935  var PodRow = cr.ui.define('podrow');
1936
1937  PodRow.prototype = {
1938    __proto__: HTMLDivElement.prototype,
1939
1940    // Whether this user pod row is shown for the first time.
1941    firstShown_: true,
1942
1943    // True if inside focusPod().
1944    insideFocusPod_: false,
1945
1946    // Focused pod.
1947    focusedPod_: undefined,
1948
1949    // Activated pod, i.e. the pod of current login attempt.
1950    activatedPod_: undefined,
1951
1952    // Pod that was most recently focused, if any.
1953    lastFocusedPod_: undefined,
1954
1955    // Pods whose initial images haven't been loaded yet.
1956    podsWithPendingImages_: [],
1957
1958    // Whether pod placement has been postponed.
1959    podPlacementPostponed_: false,
1960
1961    // Standard user pod height/width.
1962    userPodHeight_: 0,
1963    userPodWidth_: 0,
1964
1965    // Array of apps that are shown in addition to other user pods.
1966    apps_: [],
1967
1968    // True to show app pods along with user pods.
1969    shouldShowApps_: true,
1970
1971    // Array of users that are shown (public/supervised/regular).
1972    users_: [],
1973
1974    // If we're disabling single pod autofocus for Touch View.
1975    touchViewSinglePodExperimentOn_: true,
1976
1977
1978    /** @override */
1979    decorate: function() {
1980      // Event listeners that are installed for the time period during which
1981      // the element is visible.
1982      this.listeners_ = {
1983        focus: [this.handleFocus_.bind(this), true /* useCapture */],
1984        click: [this.handleClick_.bind(this), true],
1985        mousemove: [this.handleMouseMove_.bind(this), false],
1986        keydown: [this.handleKeyDown.bind(this), false]
1987      };
1988
1989      var isDesktopUserManager = Oobe.getInstance().displayType ==
1990          DISPLAY_TYPE.DESKTOP_USER_MANAGER;
1991      this.userPodHeight_ = isDesktopUserManager ? DESKTOP_POD_HEIGHT :
1992                                                   CROS_POD_HEIGHT;
1993      // Same for Chrome OS and desktop.
1994      this.userPodWidth_ = POD_WIDTH;
1995    },
1996
1997    /**
1998     * Returns all the pods in this pod row.
1999     * @type {NodeList}
2000     */
2001    get pods() {
2002      return Array.prototype.slice.call(this.children);
2003    },
2004
2005    /**
2006     * Return true if user pod row has only single user pod in it, which should
2007     * always be focused except desktop and touch view modes.
2008     * @type {boolean}
2009     */
2010    get alwaysFocusSinglePod() {
2011      var isDesktopUserManager = Oobe.getInstance().displayType ==
2012          DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2013
2014      return (isDesktopUserManager ||
2015              (this.touchViewSinglePodExperimentOn_ &&
2016               this.touchViewEnabled_)) ?
2017          false : this.children.length == 1;
2018    },
2019
2020    /**
2021     * Returns pod with the given app id.
2022     * @param {!string} app_id Application id to be matched.
2023     * @return {Object} Pod with the given app id. null if pod hasn't been
2024     *     found.
2025     */
2026    getPodWithAppId_: function(app_id) {
2027      for (var i = 0, pod; pod = this.pods[i]; ++i) {
2028        if (pod.user.isApp && pod.user.id == app_id)
2029          return pod;
2030      }
2031      return null;
2032    },
2033
2034    /**
2035     * Returns pod with the given username (null if there is no such pod).
2036     * @param {string} username Username to be matched.
2037     * @return {Object} Pod with the given username. null if pod hasn't been
2038     *     found.
2039     */
2040    getPodWithUsername_: function(username) {
2041      for (var i = 0, pod; pod = this.pods[i]; ++i) {
2042        if (pod.user.username == username)
2043          return pod;
2044      }
2045      return null;
2046    },
2047
2048    /**
2049     * True if the the pod row is disabled (handles no user interaction).
2050     * @type {boolean}
2051     */
2052    disabled_: false,
2053    get disabled() {
2054      return this.disabled_;
2055    },
2056    set disabled(value) {
2057      this.disabled_ = value;
2058      var controls = this.querySelectorAll('button,input');
2059      for (var i = 0, control; control = controls[i]; ++i) {
2060        control.disabled = value;
2061      }
2062    },
2063
2064    /**
2065     * Creates a user pod from given email.
2066     * @param {!Object} user User info dictionary.
2067     */
2068    createUserPod: function(user) {
2069      var userPod;
2070      if (user.isDesktopUser)
2071        userPod = new DesktopUserPod({user: user});
2072      else if (user.publicAccount)
2073        userPod = new PublicAccountUserPod({user: user});
2074      else if (user.isApp)
2075        userPod = new KioskAppPod({user: user});
2076      else
2077        userPod = new UserPod({user: user});
2078
2079      userPod.hidden = false;
2080      return userPod;
2081    },
2082
2083    /**
2084     * Add an existing user pod to this pod row.
2085     * @param {!Object} user User info dictionary.
2086     */
2087    addUserPod: function(user) {
2088      var userPod = this.createUserPod(user);
2089      this.appendChild(userPod);
2090      userPod.initialize();
2091    },
2092
2093    /**
2094     * Runs app with a given id from the list of loaded apps.
2095     * @param {!string} app_id of an app to run.
2096     * @param {boolean=} opt_diagnostic_mode Whether to run the app in
2097     *     diagnostic mode. Default is false.
2098     */
2099    findAndRunAppForTesting: function(app_id, opt_diagnostic_mode) {
2100      var app = this.getPodWithAppId_(app_id);
2101      if (app) {
2102        var activationEvent = cr.doc.createEvent('MouseEvents');
2103        var ctrlKey = opt_diagnostic_mode;
2104        activationEvent.initMouseEvent('click', true, true, null,
2105            0, 0, 0, 0, 0, ctrlKey, false, false, false, 0, null);
2106        app.dispatchEvent(activationEvent);
2107      }
2108    },
2109
2110    /**
2111     * Removes user pod from pod row.
2112     * @param {string} email User's email.
2113     */
2114    removeUserPod: function(username) {
2115      var podToRemove = this.getPodWithUsername_(username);
2116      if (podToRemove == null) {
2117        console.warn('Attempt to remove not existing pod for ' + username +
2118            '.');
2119        return;
2120      }
2121      this.removeChild(podToRemove);
2122      if (this.pods.length > 0)
2123        this.placePods_();
2124    },
2125
2126    /**
2127     * Returns index of given pod or -1 if not found.
2128     * @param {UserPod} pod Pod to look up.
2129     * @private
2130     */
2131    indexOf_: function(pod) {
2132      for (var i = 0; i < this.pods.length; ++i) {
2133        if (pod == this.pods[i])
2134          return i;
2135      }
2136      return -1;
2137    },
2138
2139    /**
2140     * Populates pod row with given existing users and start init animation.
2141     * @param {array} users Array of existing user emails.
2142     */
2143    loadPods: function(users) {
2144      this.users_ = users;
2145
2146      this.rebuildPods();
2147    },
2148
2149    /**
2150     * Scrolls focused user pod into view.
2151     */
2152    scrollFocusedPodIntoView: function() {
2153      var pod = this.focusedPod_;
2154      if (!pod)
2155        return;
2156
2157      // First check whether focused pod is already fully visible.
2158      var visibleArea = $('scroll-container');
2159      var scrollTop = visibleArea.scrollTop;
2160      var clientHeight = visibleArea.clientHeight;
2161      var podTop = $('oobe').offsetTop + pod.offsetTop;
2162      var padding = USER_POD_KEYBOARD_MIN_PADDING;
2163      if (podTop + pod.height + padding <= scrollTop + clientHeight &&
2164          podTop - padding >= scrollTop) {
2165        return;
2166      }
2167
2168      // Scroll so that user pod is as centered as possible.
2169      visibleArea.scrollTop = podTop - (clientHeight - pod.offsetHeight) / 2;
2170    },
2171
2172    /**
2173     * Rebuilds pod row using users_ and apps_ that were previously set or
2174     * updated.
2175     */
2176    rebuildPods: function() {
2177      var emptyPodRow = this.pods.length == 0;
2178
2179      // Clear existing pods.
2180      this.innerHTML = '';
2181      this.focusedPod_ = undefined;
2182      this.activatedPod_ = undefined;
2183      this.lastFocusedPod_ = undefined;
2184
2185      // Switch off animation
2186      Oobe.getInstance().toggleClass('flying-pods', false);
2187
2188      // Populate the pod row.
2189      for (var i = 0; i < this.users_.length; ++i)
2190        this.addUserPod(this.users_[i]);
2191
2192      for (var i = 0, pod; pod = this.pods[i]; ++i)
2193        this.podsWithPendingImages_.push(pod);
2194
2195      // TODO(nkostylev): Edge case handling when kiosk apps are not fitting.
2196      if (this.shouldShowApps_) {
2197        for (var i = 0; i < this.apps_.length; ++i)
2198          this.addUserPod(this.apps_[i]);
2199      }
2200
2201      // Make sure we eventually show the pod row, even if some image is stuck.
2202      setTimeout(function() {
2203        $('pod-row').classList.remove('images-loading');
2204      }, POD_ROW_IMAGES_LOAD_TIMEOUT_MS);
2205
2206      var isAccountPicker = $('login-header-bar').signinUIState ==
2207          SIGNIN_UI_STATE.ACCOUNT_PICKER;
2208
2209      // Immediately recalculate pods layout only when current UI is account
2210      // picker. Otherwise postpone it.
2211      if (isAccountPicker) {
2212        this.placePods_();
2213        this.maybePreselectPod();
2214
2215        // Without timeout changes in pods positions will be animated even
2216        // though it happened when 'flying-pods' class was disabled.
2217        setTimeout(function() {
2218          Oobe.getInstance().toggleClass('flying-pods', true);
2219        }, 0);
2220      } else {
2221        this.podPlacementPostponed_ = true;
2222
2223        // Update [Cancel] button state.
2224        if ($('login-header-bar').signinUIState ==
2225                SIGNIN_UI_STATE.GAIA_SIGNIN &&
2226            emptyPodRow &&
2227            this.pods.length > 0) {
2228          login.GaiaSigninScreen.updateCancelButtonState();
2229        }
2230      }
2231    },
2232
2233    /**
2234     * Adds given apps to the pod row.
2235     * @param {array} apps Array of apps.
2236     */
2237    setApps: function(apps) {
2238      this.apps_ = apps;
2239      this.rebuildPods();
2240      chrome.send('kioskAppsLoaded');
2241
2242      // Check whether there's a pending kiosk app error.
2243      window.setTimeout(function() {
2244        chrome.send('checkKioskAppLaunchError');
2245      }, 500);
2246    },
2247
2248    /**
2249     * Sets whether should show app pods.
2250     * @param {boolean} shouldShowApps Whether app pods should be shown.
2251     */
2252    setShouldShowApps: function(shouldShowApps) {
2253      if (this.shouldShowApps_ == shouldShowApps)
2254        return;
2255
2256      this.shouldShowApps_ = shouldShowApps;
2257      this.rebuildPods();
2258    },
2259
2260    /**
2261     * Shows a custom icon on a user pod besides the input field.
2262     * @param {string} username Username of pod to add button
2263     * @param {!{id: !string,
2264     *           hardlockOnClick: boolean,
2265     *           ariaLabel: string | undefined,
2266     *           tooltip: ({text: string, autoshow: boolean} | undefined)}} icon
2267     *     The icon parameters.
2268     */
2269    showUserPodCustomIcon: function(username, icon) {
2270      var pod = this.getPodWithUsername_(username);
2271      if (pod == null) {
2272        console.error('Unable to show user pod button for ' + username +
2273                      ': user pod not found.');
2274        return;
2275      }
2276
2277      if (!icon.id && !icon.tooltip)
2278        return;
2279
2280      if (icon.id)
2281        pod.customIconElement.setIcon(icon.id);
2282
2283      if (icon.hardlockOnClick) {
2284        pod.customIconElement.setInteractive(
2285            this.hardlockUserPod_.bind(this, username));
2286      } else {
2287        pod.customIconElement.setInteractive(null);
2288      }
2289
2290      var ariaLabel = icon.ariaLabel || (icon.tooltip && icon.tooltip.text);
2291      if (ariaLabel)
2292        pod.customIconElement.setAriaLabel(ariaLabel);
2293      else
2294        console.warn('No ARIA label for user pod custom icon.');
2295
2296      pod.customIconElement.show();
2297
2298      // This has to be called after |show| in case the tooltip should be shown
2299      // immediatelly.
2300      pod.customIconElement.setTooltip(
2301          icon.tooltip || {text: '', autoshow: false});
2302    },
2303
2304    /**
2305     * Hard-locks user pod for the user. If user pod is hard-locked, it can be
2306     * only unlocked using password, and the authentication type cannot be
2307     * changed.
2308     * @param {!string} username The user's username.
2309     * @private
2310     */
2311    hardlockUserPod_: function(username) {
2312      chrome.send('hardlockPod', [username]);
2313    },
2314
2315    /**
2316     * Hides the custom icon in the user pod added by showUserPodCustomIcon().
2317     * @param {string} username Username of pod to remove button
2318     */
2319    hideUserPodCustomIcon: function(username) {
2320      var pod = this.getPodWithUsername_(username);
2321      if (pod == null) {
2322        console.error('Unable to hide user pod button for ' + username +
2323                      ': user pod not found.');
2324        return;
2325      }
2326
2327      // TODO(tengs): Allow option for a fading transition.
2328      pod.customIconElement.hide();
2329    },
2330
2331    /**
2332     * Sets the authentication type used to authenticate the user.
2333     * @param {string} username Username of selected user
2334     * @param {number} authType Authentication type, must be one of the
2335     *                          values listed in AUTH_TYPE enum.
2336     * @param {string} value The initial value to use for authentication.
2337     */
2338    setAuthType: function(username, authType, value) {
2339      var pod = this.getPodWithUsername_(username);
2340      if (pod == null) {
2341        console.error('Unable to set auth type for ' + username +
2342                      ': user pod not found.');
2343        return;
2344      }
2345      pod.setAuthType(authType, value);
2346    },
2347
2348    /**
2349     * Sets the state of touch view mode.
2350     * @param {boolean} isTouchViewEnabled true if the mode is on.
2351     */
2352    setTouchViewState: function(isTouchViewEnabled) {
2353      this.touchViewEnabled_ = isTouchViewEnabled;
2354    },
2355
2356    /**
2357     * Updates the display name shown on a public session pod.
2358     * @param {string} userID The user ID of the public session
2359     * @param {string} displayName The new display name
2360     */
2361    setPublicSessionDisplayName: function(userID, displayName) {
2362      var pod = this.getPodWithUsername_(userID);
2363      if (pod != null)
2364        pod.setDisplayName(displayName);
2365    },
2366
2367    /**
2368     * Updates the list of locales available for a public session.
2369     * @param {string} userID The user ID of the public session
2370     * @param {!Object} locales The list of available locales
2371     * @param {string} defaultLocale The locale to select by default
2372     * @param {boolean} multipleRecommendedLocales Whether |locales| contains
2373     *     two or more recommended locales
2374     */
2375    setPublicSessionLocales: function(userID,
2376                                      locales,
2377                                      defaultLocale,
2378                                      multipleRecommendedLocales) {
2379      var pod = this.getPodWithUsername_(userID);
2380      if (pod != null) {
2381        pod.populateLanguageSelect(locales,
2382                                   defaultLocale,
2383                                   multipleRecommendedLocales);
2384      }
2385    },
2386
2387    /**
2388     * Updates the list of available keyboard layouts for a public session pod.
2389     * @param {string} userID The user ID of the public session
2390     * @param {string} locale The locale to which this list of keyboard layouts
2391     *     applies
2392     * @param {!Object} list List of available keyboard layouts
2393     */
2394    setPublicSessionKeyboardLayouts: function(userID, locale, list) {
2395      var pod = this.getPodWithUsername_(userID);
2396      if (pod != null)
2397        pod.populateKeyboardSelect(locale, list);
2398    },
2399
2400    /**
2401     * Called when window was resized.
2402     */
2403    onWindowResize: function() {
2404      var layout = this.calculateLayout_();
2405      if (layout.columns != this.columns || layout.rows != this.rows)
2406        this.placePods_();
2407
2408      if (Oobe.getInstance().virtualKeyboardShown)
2409        this.scrollFocusedPodIntoView();
2410    },
2411
2412    /**
2413     * Returns width of podrow having |columns| number of columns.
2414     * @private
2415     */
2416    columnsToWidth_: function(columns) {
2417      var isDesktopUserManager = Oobe.getInstance().displayType ==
2418          DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2419      var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] :
2420                                          MARGIN_BY_COLUMNS[columns];
2421      var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
2422                                              POD_ROW_PADDING;
2423      return 2 * rowPadding + columns * this.userPodWidth_ +
2424          (columns - 1) * margin;
2425    },
2426
2427    /**
2428     * Returns height of podrow having |rows| number of rows.
2429     * @private
2430     */
2431    rowsToHeight_: function(rows) {
2432      var isDesktopUserManager = Oobe.getInstance().displayType ==
2433          DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2434      var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
2435                                              POD_ROW_PADDING;
2436      return 2 * rowPadding + rows * this.userPodHeight_;
2437    },
2438
2439    /**
2440     * Calculates number of columns and rows that podrow should have in order to
2441     * hold as much its pods as possible for current screen size. Also it tries
2442     * to choose layout that looks good.
2443     * @return {{columns: number, rows: number}}
2444     */
2445    calculateLayout_: function() {
2446      var preferredColumns = this.pods.length < COLUMNS.length ?
2447          COLUMNS[this.pods.length] : COLUMNS[COLUMNS.length - 1];
2448      var maxWidth = Oobe.getInstance().clientAreaSize.width;
2449      var columns = preferredColumns;
2450      while (maxWidth < this.columnsToWidth_(columns) && columns > 1)
2451        --columns;
2452      var rows = Math.floor((this.pods.length - 1) / columns) + 1;
2453      if (getComputedStyle(
2454          $('signin-banner'), null).getPropertyValue('display') != 'none') {
2455        rows = Math.min(rows, MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER);
2456      }
2457      var maxHeigth = Oobe.getInstance().clientAreaSize.height;
2458      while (maxHeigth < this.rowsToHeight_(rows) && rows > 1)
2459        --rows;
2460      // One more iteration if it's not enough cells to place all pods.
2461      while (maxWidth >= this.columnsToWidth_(columns + 1) &&
2462             columns * rows < this.pods.length &&
2463             columns < MAX_NUMBER_OF_COLUMNS) {
2464         ++columns;
2465      }
2466      return {columns: columns, rows: rows};
2467    },
2468
2469    /**
2470     * Places pods onto their positions onto pod grid.
2471     * @private
2472     */
2473    placePods_: function() {
2474      var layout = this.calculateLayout_();
2475      var columns = this.columns = layout.columns;
2476      var rows = this.rows = layout.rows;
2477      var maxPodsNumber = columns * rows;
2478      var isDesktopUserManager = Oobe.getInstance().displayType ==
2479          DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2480      var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] :
2481                                          MARGIN_BY_COLUMNS[columns];
2482      this.parentNode.setPreferredSize(
2483          this.columnsToWidth_(columns), this.rowsToHeight_(rows));
2484      var height = this.userPodHeight_;
2485      var width = this.userPodWidth_;
2486      this.pods.forEach(function(pod, index) {
2487        if (index >= maxPodsNumber) {
2488           pod.hidden = true;
2489           return;
2490        }
2491        pod.hidden = false;
2492        if (pod.offsetHeight != height) {
2493          console.error('Pod offsetHeight (' + pod.offsetHeight +
2494              ') and POD_HEIGHT (' + height + ') are not equal.');
2495        }
2496        if (pod.offsetWidth != width) {
2497          console.error('Pod offsetWidth (' + pod.offsetWidth +
2498              ') and POD_WIDTH (' + width + ') are not equal.');
2499        }
2500        var column = index % columns;
2501        var row = Math.floor(index / columns);
2502        var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
2503                                                POD_ROW_PADDING;
2504        pod.left = rowPadding + column * (width + margin);
2505
2506        // On desktop, we want the rows to always be equally spaced.
2507        pod.top = isDesktopUserManager ? row * (height + rowPadding) :
2508                                         row * height + rowPadding;
2509      });
2510      Oobe.getInstance().updateScreenSize(this.parentNode);
2511    },
2512
2513    /**
2514     * Number of columns.
2515     * @type {?number}
2516     */
2517    set columns(columns) {
2518      // Cannot use 'columns' here.
2519      this.setAttribute('ncolumns', columns);
2520    },
2521    get columns() {
2522      return parseInt(this.getAttribute('ncolumns'));
2523    },
2524
2525    /**
2526     * Number of rows.
2527     * @type {?number}
2528     */
2529    set rows(rows) {
2530      // Cannot use 'rows' here.
2531      this.setAttribute('nrows', rows);
2532    },
2533    get rows() {
2534      return parseInt(this.getAttribute('nrows'));
2535    },
2536
2537    /**
2538     * Whether the pod is currently focused.
2539     * @param {UserPod} pod Pod to check for focus.
2540     * @return {boolean} Pod focus status.
2541     */
2542    isFocused: function(pod) {
2543      return this.focusedPod_ == pod;
2544    },
2545
2546    /**
2547     * Focuses a given user pod or clear focus when given null.
2548     * @param {UserPod=} podToFocus User pod to focus (undefined clears focus).
2549     * @param {boolean=} opt_force If true, forces focus update even when
2550     *     podToFocus is already focused.
2551     */
2552    focusPod: function(podToFocus, opt_force) {
2553      if (this.isFocused(podToFocus) && !opt_force) {
2554        // Calling focusPod w/o podToFocus means reset.
2555        if (!podToFocus)
2556          Oobe.clearErrors();
2557        this.keyboardActivated_ = false;
2558        return;
2559      }
2560
2561      // Make sure there's only one focusPod operation happening at a time.
2562      if (this.insideFocusPod_) {
2563        this.keyboardActivated_ = false;
2564        return;
2565      }
2566      this.insideFocusPod_ = true;
2567
2568      for (var i = 0, pod; pod = this.pods[i]; ++i) {
2569        if (!this.alwaysFocusSinglePod) {
2570          pod.isActionBoxMenuActive = false;
2571        }
2572        if (pod != podToFocus) {
2573          pod.isActionBoxMenuHovered = false;
2574          pod.classList.remove('focused');
2575          // On Desktop, the faded style is not set correctly, so we should
2576          // manually fade out non-focused pods if there is a focused pod.
2577          if (pod.user.isDesktopUser && podToFocus)
2578            pod.classList.add('faded');
2579          else
2580            pod.classList.remove('faded');
2581          pod.reset(false);
2582        }
2583      }
2584
2585      // Clear any error messages for previous pod.
2586      if (!this.isFocused(podToFocus))
2587        Oobe.clearErrors();
2588
2589      var hadFocus = !!this.focusedPod_;
2590      this.focusedPod_ = podToFocus;
2591      if (podToFocus) {
2592        podToFocus.classList.remove('faded');
2593        podToFocus.classList.add('focused');
2594        if (!podToFocus.multiProfilesPolicyApplied)
2595          podToFocus.reset(true);  // Reset and give focus.
2596        else {
2597          podToFocus.userTypeBubbleElement.classList.add('bubble-shown');
2598          podToFocus.focus();
2599        }
2600
2601        // focusPod() automatically loads wallpaper
2602        if (!podToFocus.user.isApp)
2603          chrome.send('focusPod', [podToFocus.user.username]);
2604        this.firstShown_ = false;
2605        this.lastFocusedPod_ = podToFocus;
2606
2607        if (Oobe.getInstance().virtualKeyboardShown)
2608          this.scrollFocusedPodIntoView();
2609      }
2610      this.insideFocusPod_ = false;
2611      this.keyboardActivated_ = false;
2612    },
2613
2614    /**
2615     * Resets wallpaper to the last active user's wallpaper, if any.
2616     */
2617    loadLastWallpaper: function() {
2618      if (this.lastFocusedPod_ && !this.lastFocusedPod_.user.isApp)
2619        chrome.send('loadWallpaper', [this.lastFocusedPod_.user.username]);
2620    },
2621
2622    /**
2623     * Returns the currently activated pod.
2624     * @type {UserPod}
2625     */
2626    get activatedPod() {
2627      return this.activatedPod_;
2628    },
2629
2630    /**
2631     * Sets currently activated pod.
2632     * @param {UserPod} pod Pod to check for focus.
2633     * @param {Event} e Event object.
2634     */
2635    setActivatedPod: function(pod, e) {
2636      if (pod && pod.activate(e))
2637        this.activatedPod_ = pod;
2638    },
2639
2640    /**
2641     * The pod of the signed-in user, if any; null otherwise.
2642     * @type {?UserPod}
2643     */
2644    get lockedPod() {
2645      for (var i = 0, pod; pod = this.pods[i]; ++i) {
2646        if (pod.user.signedIn)
2647          return pod;
2648      }
2649      return null;
2650    },
2651
2652    /**
2653     * The pod that is preselected on user pod row show.
2654     * @type {?UserPod}
2655     */
2656    get preselectedPod() {
2657      var isDesktopUserManager = Oobe.getInstance().displayType ==
2658          DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2659      if (isDesktopUserManager) {
2660        // On desktop, don't pre-select a pod if it's the only one.
2661        if (this.pods.length == 1)
2662          return null;
2663
2664        // The desktop User Manager can send the index of a pod that should be
2665        // initially focused in url hash.
2666        var podIndex = parseInt(window.location.hash.substr(1));
2667        if (isNaN(podIndex) || podIndex >= this.pods.length)
2668          return null;
2669        return this.pods[podIndex];
2670      }
2671
2672      var lockedPod = this.lockedPod;
2673      if (lockedPod)
2674        return lockedPod;
2675      for (var i = 0, pod; pod = this.pods[i]; ++i) {
2676        if (!pod.multiProfilesPolicyApplied) {
2677          return pod;
2678        }
2679      }
2680      return this.pods[0];
2681    },
2682
2683    /**
2684     * Resets input UI.
2685     * @param {boolean} takeFocus True to take focus.
2686     */
2687    reset: function(takeFocus) {
2688      this.disabled = false;
2689      if (this.activatedPod_)
2690        this.activatedPod_.reset(takeFocus);
2691    },
2692
2693    /**
2694     * Restores input focus to current selected pod, if there is any.
2695     */
2696    refocusCurrentPod: function() {
2697      if (this.focusedPod_ && !this.focusedPod_.multiProfilesPolicyApplied) {
2698        this.focusedPod_.focusInput();
2699      }
2700    },
2701
2702    /**
2703     * Clears focused pod password field.
2704     */
2705    clearFocusedPod: function() {
2706      if (!this.disabled && this.focusedPod_)
2707        this.focusedPod_.reset(true);
2708    },
2709
2710    /**
2711     * Shows signin UI.
2712     * @param {string} email Email for signin UI.
2713     */
2714    showSigninUI: function(email) {
2715      // Clear any error messages that might still be around.
2716      Oobe.clearErrors();
2717      this.disabled = true;
2718      this.lastFocusedPod_ = this.getPodWithUsername_(email);
2719      Oobe.showSigninUI(email);
2720    },
2721
2722    /**
2723     * Updates current image of a user.
2724     * @param {string} username User for which to update the image.
2725     */
2726    updateUserImage: function(username) {
2727      var pod = this.getPodWithUsername_(username);
2728      if (pod)
2729        pod.updateUserImage();
2730    },
2731
2732    /**
2733     * Handler of click event.
2734     * @param {Event} e Click Event object.
2735     * @private
2736     */
2737    handleClick_: function(e) {
2738      if (this.disabled)
2739        return;
2740
2741      // Clear all menus if the click is outside pod menu and its
2742      // button area.
2743      if (!findAncestorByClass(e.target, 'action-box-menu') &&
2744          !findAncestorByClass(e.target, 'action-box-area')) {
2745        for (var i = 0, pod; pod = this.pods[i]; ++i)
2746          pod.isActionBoxMenuActive = false;
2747      }
2748
2749      // Clears focus if not clicked on a pod and if there's more than one pod.
2750      var pod = findAncestorByClass(e.target, 'pod');
2751      if ((!pod || pod.parentNode != this) && !this.alwaysFocusSinglePod) {
2752        this.focusPod();
2753      }
2754
2755      if (pod)
2756        pod.isActionBoxMenuHovered = true;
2757
2758      // Return focus back to single pod.
2759      if (this.alwaysFocusSinglePod && !pod) {
2760        this.focusPod(this.focusedPod_, true /* force */);
2761        this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown');
2762        this.focusedPod_.isActionBoxMenuHovered = false;
2763      }
2764    },
2765
2766    /**
2767     * Handler of mouse move event.
2768     * @param {Event} e Click Event object.
2769     * @private
2770     */
2771    handleMouseMove_: function(e) {
2772      if (this.disabled)
2773        return;
2774      if (e.webkitMovementX == 0 && e.webkitMovementY == 0)
2775        return;
2776
2777      // Defocus (thus hide) action box, if it is focused on a user pod
2778      // and the pointer is not hovering over it.
2779      var pod = findAncestorByClass(e.target, 'pod');
2780      if (document.activeElement &&
2781          document.activeElement.parentNode != pod &&
2782          document.activeElement.classList.contains('action-box-area')) {
2783        document.activeElement.parentNode.focus();
2784      }
2785
2786      if (pod)
2787        pod.isActionBoxMenuHovered = true;
2788
2789      // Hide action boxes on other user pods.
2790      for (var i = 0, p; p = this.pods[i]; ++i)
2791        if (p != pod && !p.isActionBoxMenuActive)
2792          p.isActionBoxMenuHovered = false;
2793    },
2794
2795    /**
2796     * Handles focus event.
2797     * @param {Event} e Focus Event object.
2798     * @private
2799     */
2800    handleFocus_: function(e) {
2801      if (this.disabled)
2802        return;
2803      if (e.target.parentNode == this) {
2804        // Focus on a pod
2805        if (e.target.classList.contains('focused')) {
2806          if (!e.target.multiProfilesPolicyApplied)
2807            e.target.focusInput();
2808          else
2809            e.target.userTypeBubbleElement.classList.add('bubble-shown');
2810        } else
2811          this.focusPod(e.target);
2812        return;
2813      }
2814
2815      var pod = findAncestorByClass(e.target, 'pod');
2816      if (pod && pod.parentNode == this) {
2817        // Focus on a control of a pod but not on the action area button.
2818        if (!pod.classList.contains('focused') &&
2819            !e.target.classList.contains('action-box-button')) {
2820          this.focusPod(pod);
2821          pod.userTypeBubbleElement.classList.remove('bubble-shown');
2822          e.target.focus();
2823        }
2824        return;
2825      }
2826
2827      // Clears pod focus when we reach here. It means new focus is neither
2828      // on a pod nor on a button/input for a pod.
2829      // Do not "defocus" user pod when it is a single pod.
2830      // That means that 'focused' class will not be removed and
2831      // input field/button will always be visible.
2832      if (!this.alwaysFocusSinglePod)
2833        this.focusPod();
2834      else {
2835        // Hide user-type-bubble in case this is one pod and we lost focus of
2836        // it.
2837        this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown');
2838      }
2839    },
2840
2841    /**
2842     * Handler of keydown event.
2843     * @param {Event} e KeyDown Event object.
2844     */
2845    handleKeyDown: function(e) {
2846      if (this.disabled)
2847        return;
2848      var editing = e.target.tagName == 'INPUT' && e.target.value;
2849      switch (e.keyIdentifier) {
2850        case 'Left':
2851          if (!editing) {
2852            this.keyboardActivated_ = true;
2853            if (this.focusedPod_ && this.focusedPod_.previousElementSibling)
2854              this.focusPod(this.focusedPod_.previousElementSibling);
2855            else
2856              this.focusPod(this.lastElementChild);
2857
2858            e.stopPropagation();
2859          }
2860          break;
2861        case 'Right':
2862          if (!editing) {
2863            this.keyboardActivated_ = true;
2864            if (this.focusedPod_ && this.focusedPod_.nextElementSibling)
2865              this.focusPod(this.focusedPod_.nextElementSibling);
2866            else
2867              this.focusPod(this.firstElementChild);
2868
2869            e.stopPropagation();
2870          }
2871          break;
2872        case 'Enter':
2873          if (this.focusedPod_) {
2874            var targetTag = e.target.tagName;
2875            if (e.target == this.focusedPod_.passwordElement ||
2876                (targetTag != 'INPUT' &&
2877                 targetTag != 'BUTTON' &&
2878                 targetTag != 'A')) {
2879              this.setActivatedPod(this.focusedPod_, e);
2880              e.stopPropagation();
2881            }
2882          }
2883          break;
2884        case 'U+001B':  // Esc
2885          if (!this.alwaysFocusSinglePod)
2886            this.focusPod();
2887          break;
2888      }
2889    },
2890
2891    /**
2892     * Called right after the pod row is shown.
2893     */
2894    handleAfterShow: function() {
2895      // Without timeout changes in pods positions will be animated even though
2896      // it happened when 'flying-pods' class was disabled.
2897      setTimeout(function() {
2898        Oobe.getInstance().toggleClass('flying-pods', true);
2899      }, 0);
2900      // Force input focus for user pod on show and once transition ends.
2901      if (this.focusedPod_) {
2902        var focusedPod = this.focusedPod_;
2903        var screen = this.parentNode;
2904        var self = this;
2905        focusedPod.addEventListener('webkitTransitionEnd', function f(e) {
2906          focusedPod.removeEventListener('webkitTransitionEnd', f);
2907          focusedPod.reset(true);
2908          // Notify screen that it is ready.
2909          screen.onShow();
2910        });
2911        // Guard timer for 1 second -- it would conver all possible animations.
2912        ensureTransitionEndEvent(focusedPod, 1000);
2913      }
2914    },
2915
2916    /**
2917     * Called right before the pod row is shown.
2918     */
2919    handleBeforeShow: function() {
2920      Oobe.getInstance().toggleClass('flying-pods', false);
2921      for (var event in this.listeners_) {
2922        this.ownerDocument.addEventListener(
2923            event, this.listeners_[event][0], this.listeners_[event][1]);
2924      }
2925      $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR;
2926
2927      if (this.podPlacementPostponed_) {
2928        this.podPlacementPostponed_ = false;
2929        this.placePods_();
2930        this.maybePreselectPod();
2931      }
2932    },
2933
2934    /**
2935     * Called when the element is hidden.
2936     */
2937    handleHide: function() {
2938      for (var event in this.listeners_) {
2939        this.ownerDocument.removeEventListener(
2940            event, this.listeners_[event][0], this.listeners_[event][1]);
2941      }
2942      $('login-header-bar').buttonsTabIndex = 0;
2943    },
2944
2945    /**
2946     * Called when a pod's user image finishes loading.
2947     */
2948    handlePodImageLoad: function(pod) {
2949      var index = this.podsWithPendingImages_.indexOf(pod);
2950      if (index == -1) {
2951        return;
2952      }
2953
2954      this.podsWithPendingImages_.splice(index, 1);
2955      if (this.podsWithPendingImages_.length == 0) {
2956        this.classList.remove('images-loading');
2957      }
2958    },
2959
2960    /**
2961     * Preselects pod, if needed.
2962     */
2963     maybePreselectPod: function() {
2964       var pod = this.preselectedPod;
2965       this.focusPod(pod);
2966
2967       // Hide user-type-bubble in case all user pods are disabled and we focus
2968       // first pod.
2969       if (pod && pod.multiProfilesPolicyApplied) {
2970         pod.userTypeBubbleElement.classList.remove('bubble-shown');
2971       }
2972     }
2973  };
2974
2975  return {
2976    PodRow: PodRow
2977  };
2978});
2979