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_WIDTH = 420;
53  var CROS_POD_HEIGHT = 213;
54  var DESKTOP_POD_HEIGHT = 216;
55  var POD_ROW_PADDING = 10;
56  var DESKTOP_ROW_PADDING = 15;
57
58  /**
59   * Minimal padding between user pod and virtual keyboard.
60   * @type {number}
61   * @const
62   */
63  var USER_POD_KEYBOARD_MIN_PADDING = 20;
64
65  /**
66   * Whether to preselect the first pod automatically on login screen.
67   * @type {boolean}
68   * @const
69   */
70  var PRESELECT_FIRST_POD = true;
71
72  /**
73   * Maximum time for which the pod row remains hidden until all user images
74   * have been loaded.
75   * @type {number}
76   * @const
77   */
78  var POD_ROW_IMAGES_LOAD_TIMEOUT_MS = 3000;
79
80  /**
81   * Public session help topic identifier.
82   * @type {number}
83   * @const
84   */
85  var HELP_TOPIC_PUBLIC_SESSION = 3041033;
86
87  /**
88   * Tab order for user pods. Update these when adding new controls.
89   * @enum {number}
90   * @const
91   */
92  var UserPodTabOrder = {
93    POD_INPUT: 1,     // Password input fields (and whole pods themselves).
94    HEADER_BAR: 2,    // Buttons on the header bar (Shutdown, Add User).
95    ACTION_BOX: 3,    // Action box buttons.
96    PAD_MENU_ITEM: 4  // User pad menu items (Remove this user).
97  };
98
99  /**
100   * Supported authentication types. Keep in sync with the enum in
101   * chrome/browser/chromeos/login/login_display.h
102   * @enum {number}
103   * @const
104   */
105  var AUTH_TYPE = {
106    OFFLINE_PASSWORD: 0,
107    ONLINE_SIGN_IN: 1,
108    NUMERIC_PIN: 2,
109    USER_CLICK: 3,
110  };
111
112  /**
113   * Names of authentication types.
114   */
115  var AUTH_TYPE_NAMES = {
116    0: 'offlinePassword',
117    1: 'onlineSignIn',
118    2: 'numericPin',
119    3: 'userClick',
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) buttons on the header bar have tab index 2 so they follow user pods;
128  // (4) Action box buttons have tab index 3 and follow header bar buttons;
129  // (5) lastly, focus jumps to the Status Area and back to user pods.
130  //
131  // 'Focus' event is handled by a capture handler for the whole document
132  // and in some cases 'mousedown' event handlers are used instead of 'click'
133  // handlers where it's necessary to prevent 'focus' event from being fired.
134
135  /**
136   * Helper function to remove a class from given element.
137   * @param {!HTMLElement} el Element whose class list to change.
138   * @param {string} cl Class to remove.
139   */
140  function removeClass(el, cl) {
141    el.classList.remove(cl);
142  }
143
144  /**
145   * Creates a user pod.
146   * @constructor
147   * @extends {HTMLDivElement}
148   */
149  var UserPod = cr.ui.define(function() {
150    var node = $('user-pod-template').cloneNode(true);
151    node.removeAttribute('id');
152    return node;
153  });
154
155  /**
156   * Stops event propagation from the any user pod child element.
157   * @param {Event} e Event to handle.
158   */
159  function stopEventPropagation(e) {
160    // Prevent default so that we don't trigger a 'focus' event.
161    e.preventDefault();
162    e.stopPropagation();
163  }
164
165  /**
166   * Unique salt added to user image URLs to prevent caching. Dictionary with
167   * user names as keys.
168   * @type {Object}
169   */
170  UserPod.userImageSalt_ = {};
171
172  UserPod.prototype = {
173    __proto__: HTMLDivElement.prototype,
174
175    /** @override */
176    decorate: function() {
177      this.tabIndex = UserPodTabOrder.POD_INPUT;
178      this.actionBoxAreaElement.tabIndex = UserPodTabOrder.ACTION_BOX;
179
180      this.addEventListener('keydown', this.handlePodKeyDown_.bind(this));
181      this.addEventListener('click', this.handleClickOnPod_.bind(this));
182
183      this.signinButtonElement.addEventListener('click',
184          this.activate.bind(this));
185
186      this.actionBoxAreaElement.addEventListener('mousedown',
187                                                 stopEventPropagation);
188      this.actionBoxAreaElement.addEventListener('click',
189          this.handleActionAreaButtonClick_.bind(this));
190      this.actionBoxAreaElement.addEventListener('keydown',
191          this.handleActionAreaButtonKeyDown_.bind(this));
192
193      this.actionBoxMenuRemoveElement.addEventListener('click',
194          this.handleRemoveCommandClick_.bind(this));
195      this.actionBoxMenuRemoveElement.addEventListener('keydown',
196          this.handleRemoveCommandKeyDown_.bind(this));
197      this.actionBoxMenuRemoveElement.addEventListener('blur',
198          this.handleRemoveCommandBlur_.bind(this));
199
200      if (this.actionBoxRemoveUserWarningButtonElement) {
201        this.actionBoxRemoveUserWarningButtonElement.addEventListener(
202            'click',
203            this.handleRemoveUserConfirmationClick_.bind(this));
204      }
205    },
206
207    /**
208     * Initializes the pod after its properties set and added to a pod row.
209     */
210    initialize: function() {
211      this.passwordElement.addEventListener('keydown',
212          this.parentNode.handleKeyDown.bind(this.parentNode));
213      this.passwordElement.addEventListener('keypress',
214          this.handlePasswordKeyPress_.bind(this));
215
216      this.imageElement.addEventListener('load',
217          this.parentNode.handlePodImageLoad.bind(this.parentNode, this));
218
219      var initialAuthType = this.user.initialAuthType ||
220          AUTH_TYPE.OFFLINE_PASSWORD;
221      this.setAuthType(initialAuthType, null);
222    },
223
224    /**
225     * Resets tab order for pod elements to its initial state.
226     */
227    resetTabOrder: function() {
228      // Note: the |mainInput| can be the pod itself.
229      this.mainInput.tabIndex = -1;
230      this.tabIndex = UserPodTabOrder.POD_INPUT;
231    },
232
233    /**
234     * Handles keypress event (i.e. any textual input) on password input.
235     * @param {Event} e Keypress Event object.
236     * @private
237     */
238    handlePasswordKeyPress_: function(e) {
239      // When tabbing from the system tray a tab key press is received. Suppress
240      // this so as not to type a tab character into the password field.
241      if (e.keyCode == 9) {
242        e.preventDefault();
243        return;
244      }
245    },
246
247    /**
248     * Top edge margin number of pixels.
249     * @type {?number}
250     */
251    set top(top) {
252      this.style.top = cr.ui.toCssPx(top);
253    },
254
255    /**
256     * Top edge margin number of pixels.
257     */
258    get top() {
259      return parseInt(this.style.top);
260    },
261
262    /**
263     * Left edge margin number of pixels.
264     * @type {?number}
265     */
266    set left(left) {
267      this.style.left = cr.ui.toCssPx(left);
268    },
269
270    /**
271     * Left edge margin number of pixels.
272     */
273    get left() {
274      return parseInt(this.style.left);
275    },
276
277    /**
278     * Height number of pixels.
279     */
280    get height() {
281      return this.offsetHeight;
282    },
283
284    /**
285     * Gets signed in indicator element.
286     * @type {!HTMLDivElement}
287     */
288    get signedInIndicatorElement() {
289      return this.querySelector('.signed-in-indicator');
290    },
291
292    /**
293     * Gets image element.
294     * @type {!HTMLImageElement}
295     */
296    get imageElement() {
297      return this.querySelector('.user-image');
298    },
299
300    /**
301     * Gets name element.
302     * @type {!HTMLDivElement}
303     */
304    get nameElement() {
305      return this.querySelector('.name');
306    },
307
308    /**
309     * Gets password field.
310     * @type {!HTMLInputElement}
311     */
312    get passwordElement() {
313      return this.querySelector('.password');
314    },
315
316    /**
317     * Gets the password label, which is used to show a message where the
318     * password field is normally.
319     * @type {!HTMLInputElement}
320     */
321    get passwordLabelElement() {
322      return this.querySelector('.password-label');
323    },
324
325    /**
326     * Gets Caps Lock hint image.
327     * @type {!HTMLImageElement}
328     */
329    get capslockHintElement() {
330      return this.querySelector('.capslock-hint');
331    },
332
333    /**
334     * Gets user sign in button.
335     * @type {!HTMLButtonElement}
336     */
337    get signinButtonElement() {
338      return this.querySelector('.signin-button');
339    },
340
341    /**
342     * Gets launch app button.
343     * @type {!HTMLButtonElement}
344     */
345    get launchAppButtonElement() {
346      return this.querySelector('.launch-app-button');
347    },
348
349    /**
350     * Gets action box area.
351     * @type {!HTMLInputElement}
352     */
353    get actionBoxAreaElement() {
354      return this.querySelector('.action-box-area');
355    },
356
357    /**
358     * Gets user type icon area.
359     * @type {!HTMLDivElement}
360     */
361    get userTypeIconAreaElement() {
362      return this.querySelector('.user-type-icon-area');
363    },
364
365    /**
366     * Gets user type bubble like multi-profiles policy restriction message.
367     * @type {!HTMLDivElement}
368     */
369    get userTypeBubbleElement() {
370      return this.querySelector('.user-type-bubble');
371    },
372
373    /**
374     * Gets user type icon.
375     * @type {!HTMLDivElement}
376     */
377    get userTypeIconElement() {
378      return this.querySelector('.user-type-icon-image');
379    },
380
381    /**
382     * Gets action box menu.
383     * @type {!HTMLInputElement}
384     */
385    get actionBoxMenuElement() {
386      return this.querySelector('.action-box-menu');
387    },
388
389    /**
390     * Gets action box menu title.
391     * @type {!HTMLInputElement}
392     */
393    get actionBoxMenuTitleElement() {
394      return this.querySelector('.action-box-menu-title');
395    },
396
397    /**
398     * Gets action box menu title, user name item.
399     * @type {!HTMLInputElement}
400     */
401    get actionBoxMenuTitleNameElement() {
402      return this.querySelector('.action-box-menu-title-name');
403    },
404
405    /**
406     * Gets action box menu title, user email item.
407     * @type {!HTMLInputElement}
408     */
409    get actionBoxMenuTitleEmailElement() {
410      return this.querySelector('.action-box-menu-title-email');
411    },
412
413    /**
414     * Gets action box menu, remove user command item.
415     * @type {!HTMLInputElement}
416     */
417    get actionBoxMenuCommandElement() {
418      return this.querySelector('.action-box-menu-remove-command');
419    },
420
421    /**
422     * Gets action box menu, remove user command item div.
423     * @type {!HTMLInputElement}
424     */
425    get actionBoxMenuRemoveElement() {
426      return this.querySelector('.action-box-menu-remove');
427    },
428
429    /**
430     * Gets action box menu, remove user warning text div.
431     * @type {!HTMLInputElement}
432     */
433    get actionBoxRemoveUserWarningTextElement() {
434      return this.querySelector('.action-box-remove-user-warning-text');
435    },
436
437    /**
438     * Gets action box menu, remove supervised user warning text div.
439     * @type {!HTMLInputElement}
440     */
441    get actionBoxRemoveSupervisedUserWarningTextElement() {
442      return this.querySelector(
443          '.action-box-remove-supervised-user-warning-text');
444    },
445
446    /**
447     * Gets action box menu, remove user command item div.
448     * @type {!HTMLInputElement}
449     */
450    get actionBoxRemoveUserWarningElement() {
451      return this.querySelector('.action-box-remove-user-warning');
452    },
453
454    /**
455     * Gets action box menu, remove user command item div.
456     * @type {!HTMLInputElement}
457     */
458    get actionBoxRemoveUserWarningButtonElement() {
459      return this.querySelector('.remove-warning-button');
460    },
461
462    /**
463     * Gets the locked user indicator box.
464     * @type {!HTMLInputElement}
465     */
466    get lockedIndicatorElement() {
467      return this.querySelector('.locked-indicator');
468    },
469
470    /**
471     * Gets the supervised user indicator box.
472     * @type {!HTMLInputElement}
473     */
474    get supervisedUserIndicatorElement() {
475      return this.querySelector('.supervised-indicator');
476    },
477
478    /**
479     * Gets the custom icon. This icon is normally hidden, but can be shown
480     * using the chrome.screenlockPrivate API.
481     * @type {!HTMLDivElement}
482     */
483    get customIconElement() {
484      return this.querySelector('.custom-icon');
485    },
486
487    /**
488     * Updates the user pod element.
489     */
490    update: function() {
491      this.imageElement.src = 'chrome://userimage/' + this.user.username +
492          '?id=' + UserPod.userImageSalt_[this.user.username];
493
494      this.nameElement.textContent = this.user_.displayName;
495      this.signedInIndicatorElement.hidden = !this.user_.signedIn;
496
497      this.signinButtonElement.hidden = !this.isAuthTypeOnlineSignIn;
498      if (this.isAuthTypeUserClick)
499        this.passwordLabelElement.textContent = this.authValue;
500
501      this.updateActionBoxArea();
502
503      this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF(
504        'passwordFieldAccessibleName', this.user_.emailAddress));
505
506      this.customizeUserPodPerUserType();
507    },
508
509    updateActionBoxArea: function() {
510      if (this.user_.publicAccount || this.user_.isApp) {
511        this.actionBoxAreaElement.hidden = true;
512        return;
513      }
514
515      this.actionBoxAreaElement.hidden = false;
516      this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
517
518      this.actionBoxAreaElement.setAttribute(
519          'aria-label', loadTimeData.getStringF(
520              'podMenuButtonAccessibleName', this.user_.emailAddress));
521      this.actionBoxMenuRemoveElement.setAttribute(
522          'aria-label', loadTimeData.getString(
523               'podMenuRemoveItemAccessibleName'));
524      this.actionBoxMenuTitleNameElement.textContent = this.user_.isOwner ?
525          loadTimeData.getStringF('ownerUserPattern', this.user_.displayName) :
526          this.user_.displayName;
527      this.actionBoxMenuTitleEmailElement.textContent = this.user_.emailAddress;
528      this.actionBoxMenuTitleEmailElement.hidden =
529          this.user_.locallyManagedUser;
530
531      this.actionBoxMenuCommandElement.textContent =
532          loadTimeData.getString('removeUser');
533    },
534
535    customizeUserPodPerUserType: function() {
536      if (this.user_.locallyManagedUser && !this.user_.isDesktopUser) {
537        this.setUserPodIconType('supervised');
538      } else if (this.multiProfilesPolicyApplied) {
539        // Mark user pod as not focusable which in addition to the grayed out
540        // filter makes it look in disabled state.
541        this.classList.add('not-focusable');
542        this.setUserPodIconType('policy');
543
544        this.querySelector('.mp-policy-title').hidden = false;
545        if (this.user.multiProfilesPolicy == 'primary-only')
546          this.querySelector('.mp-policy-primary-only-msg').hidden = false;
547        else if (this.user.multiProfilesPolicy == 'owner-primary-only')
548          this.querySelector('.mp-owner-primary-only-msg').hidden = false;
549        else
550          this.querySelector('.mp-policy-not-allowed-msg').hidden = false;
551      } else if (this.user_.isApp) {
552        this.setUserPodIconType('app');
553      }
554    },
555
556    setUserPodIconType: function(userTypeClass) {
557      this.userTypeIconAreaElement.classList.add(userTypeClass);
558      this.userTypeIconAreaElement.hidden = false;
559    },
560
561    /**
562     * The user that this pod represents.
563     * @type {!Object}
564     */
565    user_: undefined,
566    get user() {
567      return this.user_;
568    },
569    set user(userDict) {
570      this.user_ = userDict;
571      this.update();
572    },
573
574    /**
575     * Returns true if multi-profiles sign in is currently active and this
576     * user pod is restricted per policy.
577     * @type {boolean}
578     */
579    get multiProfilesPolicyApplied() {
580      var isMultiProfilesUI =
581        (Oobe.getInstance().displayType == DISPLAY_TYPE.USER_ADDING);
582      return isMultiProfilesUI && !this.user_.isMultiProfilesAllowed;
583    },
584
585    /**
586     * Gets main input element.
587     * @type {(HTMLButtonElement|HTMLInputElement)}
588     */
589    get mainInput() {
590      if (this.isAuthTypePassword) {
591        return this.passwordElement;
592      } else if (this.isAuthTypeOnlineSignIn) {
593        return this.signinButtonElement;
594      } else if (this.isAuthTypeUserClick) {
595        return this;
596      }
597    },
598
599    /**
600     * Whether action box button is in active state.
601     * @type {boolean}
602     */
603    get isActionBoxMenuActive() {
604      return this.actionBoxAreaElement.classList.contains('active');
605    },
606    set isActionBoxMenuActive(active) {
607      if (active == this.isActionBoxMenuActive)
608        return;
609
610      if (active) {
611        this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
612        if (this.actionBoxRemoveUserWarningElement)
613          this.actionBoxRemoveUserWarningElement.hidden = true;
614
615        // Clear focus first if another pod is focused.
616        if (!this.parentNode.isFocused(this)) {
617          this.parentNode.focusPod(undefined, true);
618          this.actionBoxAreaElement.focus();
619        }
620        this.actionBoxAreaElement.classList.add('active');
621      } else {
622        this.actionBoxAreaElement.classList.remove('active');
623      }
624    },
625
626    /**
627     * Whether action box button is in hovered state.
628     * @type {boolean}
629     */
630    get isActionBoxMenuHovered() {
631      return this.actionBoxAreaElement.classList.contains('hovered');
632    },
633    set isActionBoxMenuHovered(hovered) {
634      if (hovered == this.isActionBoxMenuHovered)
635        return;
636
637      if (hovered) {
638        this.actionBoxAreaElement.classList.add('hovered');
639        this.classList.add('hovered');
640      } else {
641        if (this.multiProfilesPolicyApplied)
642          this.userTypeBubbleElement.classList.remove('bubble-shown');
643        this.actionBoxAreaElement.classList.remove('hovered');
644        this.classList.remove('hovered');
645      }
646    },
647
648    /**
649     * Set the authentication type for the pod.
650     * @param {number} An auth type value defined in the AUTH_TYPE enum.
651     * @param {string} authValue The initial value used for the auth type.
652     */
653    setAuthType: function(authType, authValue) {
654      this.authType_ = authType;
655      this.authValue_ = authValue;
656      this.setAttribute('auth-type', AUTH_TYPE_NAMES[this.authType_]);
657      this.update();
658      this.reset(this.parentNode.isFocused(this));
659    },
660
661    /**
662     * The auth type of the user pod. This value is one of the enum
663     * values in AUTH_TYPE.
664     * @type {number}
665     */
666    get authType() {
667      return this.authType_;
668    },
669
670    /**
671     * The initial value used for the pod's authentication type.
672     * eg. a prepopulated password input when using password authentication.
673     */
674    get authValue() {
675      return this.authValue_;
676    },
677
678    /**
679     * True if the the user pod uses a password to authenticate.
680     * @type {bool}
681     */
682    get isAuthTypePassword() {
683      return this.authType_ == AUTH_TYPE.OFFLINE_PASSWORD;
684    },
685
686    /**
687     * True if the the user pod uses a user click to authenticate.
688     * @type {bool}
689     */
690    get isAuthTypeUserClick() {
691      return this.authType_ == AUTH_TYPE.USER_CLICK;
692    },
693
694    /**
695     * True if the the user pod uses a online sign in to authenticate.
696     * @type {bool}
697     */
698    get isAuthTypeOnlineSignIn() {
699      return this.authType_ == AUTH_TYPE.ONLINE_SIGN_IN;
700    },
701
702    /**
703     * Updates the image element of the user.
704     */
705    updateUserImage: function() {
706      UserPod.userImageSalt_[this.user.username] = new Date().getTime();
707      this.update();
708    },
709
710    /**
711     * Focuses on input element.
712     */
713    focusInput: function() {
714      // Move tabIndex from the whole pod to the main input.
715      // Note: the |mainInput| can be the pod itself.
716      this.tabIndex = -1;
717      this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
718      this.mainInput.focus();
719    },
720
721    /**
722     * Activates the pod.
723     * @param {Event} e Event object.
724     * @return {boolean} True if activated successfully.
725     */
726    activate: function(e) {
727      if (this.isAuthTypeOnlineSignIn) {
728        this.showSigninUI();
729      } else if (this.isAuthTypeUserClick) {
730        Oobe.disableSigninUI();
731        chrome.send('attemptUnlock', [this.user.username]);
732      } else if (this.isAuthTypePassword) {
733        if (!this.passwordElement.value)
734          return false;
735        Oobe.disableSigninUI();
736        chrome.send('authenticateUser',
737                    [this.user.username, this.passwordElement.value]);
738      } else {
739        console.error('Activating user pod with invalid authentication type: ' +
740            this.authType);
741      }
742
743      return true;
744    },
745
746    showSupervisedUserSigninWarning: function() {
747      // Locally managed user token has been invalidated.
748      // Make sure that pod is focused i.e. "Sign in" button is seen.
749      this.parentNode.focusPod(this);
750
751      var error = document.createElement('div');
752      var messageDiv = document.createElement('div');
753      messageDiv.className = 'error-message-bubble';
754      messageDiv.textContent =
755          loadTimeData.getString('supervisedUserExpiredTokenWarning');
756      error.appendChild(messageDiv);
757
758      $('bubble').showContentForElement(
759          this.signinButtonElement,
760          cr.ui.Bubble.Attachment.TOP,
761          error,
762          this.signinButtonElement.offsetWidth / 2,
763          4);
764    },
765
766    /**
767     * Shows signin UI for this user.
768     */
769    showSigninUI: function() {
770      if (this.user.locallyManagedUser && !this.user.isDesktopUser) {
771        this.showSupervisedUserSigninWarning();
772      } else {
773        // Special case for multi-profiles sign in. We show users even if they
774        // are not allowed per policy. Restrict those users from starting GAIA.
775        if (this.multiProfilesPolicyApplied)
776          return;
777
778        this.parentNode.showSigninUI(this.user.emailAddress);
779      }
780    },
781
782    /**
783     * Resets the input field and updates the tab order of pod controls.
784     * @param {boolean} takeFocus If true, input field takes focus.
785     */
786    reset: function(takeFocus) {
787      this.passwordElement.value = '';
788      if (takeFocus)
789        this.focusInput();  // This will set a custom tab order.
790      else
791        this.resetTabOrder();
792    },
793
794    /**
795     * Handles a click event on action area button.
796     * @param {Event} e Click event.
797     */
798    handleActionAreaButtonClick_: function(e) {
799      if (this.parentNode.disabled)
800        return;
801      this.isActionBoxMenuActive = !this.isActionBoxMenuActive;
802      e.stopPropagation();
803    },
804
805    /**
806     * Handles a keydown event on action area button.
807     * @param {Event} e KeyDown event.
808     */
809    handleActionAreaButtonKeyDown_: function(e) {
810      if (this.disabled)
811        return;
812      switch (e.keyIdentifier) {
813        case 'Enter':
814        case 'U+0020':  // Space
815          if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive)
816            this.isActionBoxMenuActive = true;
817          e.stopPropagation();
818          break;
819        case 'Up':
820        case 'Down':
821          if (this.isActionBoxMenuActive) {
822            this.actionBoxMenuRemoveElement.tabIndex =
823                UserPodTabOrder.PAD_MENU_ITEM;
824            this.actionBoxMenuRemoveElement.focus();
825          }
826          e.stopPropagation();
827          break;
828        case 'U+001B':  // Esc
829          this.isActionBoxMenuActive = false;
830          e.stopPropagation();
831          break;
832        case 'U+0009':  // Tab
833          this.parentNode.focusPod();
834        default:
835          this.isActionBoxMenuActive = false;
836          break;
837      }
838    },
839
840    /**
841     * Handles a click event on remove user command.
842     * @param {Event} e Click event.
843     */
844    handleRemoveCommandClick_: function(e) {
845      if (this.user.locallyManagedUser || this.user.isDesktopUser) {
846        this.showRemoveWarning_();
847        return;
848      }
849      if (this.isActionBoxMenuActive)
850        chrome.send('removeUser', [this.user.username]);
851    },
852
853    /**
854     * Shows remove user warning. Used for supervised users on CrOS, and for all
855     * users on desktop.
856     */
857    showRemoveWarning_: function() {
858      this.actionBoxMenuRemoveElement.hidden = true;
859      this.actionBoxRemoveUserWarningElement.hidden = false;
860    },
861
862    /**
863     * Handles a click event on remove user confirmation button.
864     * @param {Event} e Click event.
865     */
866    handleRemoveUserConfirmationClick_: function(e) {
867      if (this.isActionBoxMenuActive)
868        chrome.send('removeUser', [this.user.username]);
869    },
870
871    /**
872     * Handles a keydown event on remove command.
873     * @param {Event} e KeyDown event.
874     */
875    handleRemoveCommandKeyDown_: function(e) {
876      if (this.disabled)
877        return;
878      switch (e.keyIdentifier) {
879        case 'Enter':
880          chrome.send('removeUser', [this.user.username]);
881          e.stopPropagation();
882          break;
883        case 'Up':
884        case 'Down':
885          e.stopPropagation();
886          break;
887        case 'U+001B':  // Esc
888          this.actionBoxAreaElement.focus();
889          this.isActionBoxMenuActive = false;
890          e.stopPropagation();
891          break;
892        default:
893          this.actionBoxAreaElement.focus();
894          this.isActionBoxMenuActive = false;
895          break;
896      }
897    },
898
899    /**
900     * Handles a blur event on remove command.
901     * @param {Event} e Blur event.
902     */
903    handleRemoveCommandBlur_: function(e) {
904      if (this.disabled)
905        return;
906      this.actionBoxMenuRemoveElement.tabIndex = -1;
907    },
908
909    /**
910     * Handles click event on a user pod.
911     * @param {Event} e Click event.
912     */
913    handleClickOnPod_: function(e) {
914      if (this.parentNode.disabled)
915        return;
916
917      if (!this.isActionBoxMenuActive) {
918        if (this.isAuthTypeOnlineSignIn) {
919          this.showSigninUI();
920        } else if (this.isAuthTypeUserClick) {
921          this.parentNode.setActivatedPod(this);
922        }
923
924        if (this.multiProfilesPolicyApplied)
925          this.userTypeBubbleElement.classList.add('bubble-shown');
926
927        // Prevent default so that we don't trigger 'focus' event.
928        e.preventDefault();
929      }
930    },
931
932    /**
933     * Handles keydown event for a user pod.
934     * @param {Event} e Key event.
935     */
936    handlePodKeyDown_: function(e) {
937      if (!this.isAuthTypeUserClick || this.disabled)
938        return;
939      switch (e.keyIdentifier) {
940        case 'Enter':
941        case 'U+0020':  // Space
942          if (this.parentNode.isFocused(this))
943            this.parentNode.setActivatedPod(this);
944          break;
945      }
946    }
947  };
948
949  /**
950   * Creates a public account user pod.
951   * @constructor
952   * @extends {UserPod}
953   */
954  var PublicAccountUserPod = cr.ui.define(function() {
955    var node = UserPod();
956
957    var extras = $('public-account-user-pod-extras-template').children;
958    for (var i = 0; i < extras.length; ++i) {
959      var el = extras[i].cloneNode(true);
960      node.appendChild(el);
961    }
962
963    return node;
964  });
965
966  PublicAccountUserPod.prototype = {
967    __proto__: UserPod.prototype,
968
969    /**
970     * "Enter" button in expanded side pane.
971     * @type {!HTMLButtonElement}
972     */
973    get enterButtonElement() {
974      return this.querySelector('.enter-button');
975    },
976
977    /**
978     * Boolean flag of whether the pod is showing the side pane. The flag
979     * controls whether 'expanded' class is added to the pod's class list and
980     * resets tab order because main input element changes when the 'expanded'
981     * state changes.
982     * @type {boolean}
983     */
984    get expanded() {
985      return this.classList.contains('expanded');
986    },
987
988    /**
989     * During transition final height of pod is not available because of
990     * flexbox layout. That's why we have to calculate
991     * the final height manually.
992     */
993    get expandedHeight_() {
994      function getTopAndBottomPadding(domElement) {
995        return parseInt(window.getComputedStyle(
996            domElement).getPropertyValue('padding-top')) +
997            parseInt(window.getComputedStyle(
998                domElement).getPropertyValue('padding-bottom'));
999      };
1000      var height =
1001        this.getElementsByClassName('side-pane-contents')[0].offsetHeight +
1002        this.getElementsByClassName('enter-button')[0].offsetHeight +
1003        getTopAndBottomPadding(
1004            this.getElementsByClassName('enter-button')[0]) +
1005        getTopAndBottomPadding(
1006            this.getElementsByClassName('side-pane-container')[0]) +
1007        getTopAndBottomPadding(this);
1008      return height;
1009    },
1010
1011    set expanded(expanded) {
1012      if (this.expanded == expanded)
1013        return;
1014
1015      this.resetTabOrder();
1016      this.classList.toggle('expanded', expanded);
1017      if (expanded) {
1018        var isDesktopUserManager = Oobe.getInstance().displayType ==
1019            DISPLAY_TYPE.DESKTOP_USER_MANAGER;
1020        var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
1021                                                POD_ROW_PADDING;
1022        this.usualLeft = this.left;
1023        this.usualTop = this.top;
1024        if (this.left + PUBLIC_EXPANDED_WIDTH >
1025            $('pod-row').offsetWidth - rowPadding)
1026          this.left = $('pod-row').offsetWidth - rowPadding -
1027              PUBLIC_EXPANDED_WIDTH;
1028        var expandedHeight = this.expandedHeight_;
1029        if (this.top + expandedHeight > $('pod-row').offsetHeight)
1030          this.top = $('pod-row').offsetHeight - expandedHeight;
1031      } else {
1032        if (typeof(this.usualLeft) != 'undefined')
1033          this.left = this.usualLeft;
1034        if (typeof(this.usualTop) != 'undefined')
1035          this.top = this.usualTop;
1036      }
1037
1038      var self = this;
1039      this.classList.add('animating');
1040      this.addEventListener('webkitTransitionEnd', function f(e) {
1041        self.removeEventListener('webkitTransitionEnd', f);
1042        self.classList.remove('animating');
1043
1044        // Accessibility focus indicator does not move with the focused
1045        // element. Sends a 'focus' event on the currently focused element
1046        // so that accessibility focus indicator updates its location.
1047        if (document.activeElement)
1048          document.activeElement.dispatchEvent(new Event('focus'));
1049      });
1050    },
1051
1052    /** @override */
1053    get mainInput() {
1054      if (this.expanded)
1055        return this.enterButtonElement;
1056      else
1057        return this.nameElement;
1058    },
1059
1060    /** @override */
1061    decorate: function() {
1062      UserPod.prototype.decorate.call(this);
1063
1064      this.classList.remove('need-password');
1065      this.classList.add('public-account');
1066
1067      this.nameElement.addEventListener('keydown', (function(e) {
1068        if (e.keyIdentifier == 'Enter') {
1069          this.parentNode.setActivatedPod(this, e);
1070          // Stop this keydown event from bubbling up to PodRow handler.
1071          e.stopPropagation();
1072          // Prevent default so that we don't trigger a 'click' event on the
1073          // newly focused "Enter" button.
1074          e.preventDefault();
1075        }
1076      }).bind(this));
1077
1078      var learnMore = this.querySelector('.learn-more');
1079      learnMore.addEventListener('mousedown', stopEventPropagation);
1080      learnMore.addEventListener('click', this.handleLearnMoreEvent);
1081      learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
1082
1083      learnMore = this.querySelector('.side-pane-learn-more');
1084      learnMore.addEventListener('click', this.handleLearnMoreEvent);
1085      learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
1086
1087      this.enterButtonElement.addEventListener('click', (function(e) {
1088        this.enterButtonElement.disabled = true;
1089        chrome.send('launchPublicAccount', [this.user.username]);
1090      }).bind(this));
1091    },
1092
1093    /** @override **/
1094    update: function() {
1095      UserPod.prototype.update.call(this);
1096      this.querySelector('.side-pane-name').textContent =
1097          this.user_.displayName;
1098      this.querySelector('.info').textContent =
1099          loadTimeData.getStringF('publicAccountInfoFormat',
1100                                  this.user_.enterpriseDomain);
1101    },
1102
1103    /** @override */
1104    focusInput: function() {
1105      // Move tabIndex from the whole pod to the main input.
1106      this.tabIndex = -1;
1107      this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1108      this.mainInput.focus();
1109    },
1110
1111    /** @override */
1112    reset: function(takeFocus) {
1113      if (!takeFocus)
1114        this.expanded = false;
1115      this.enterButtonElement.disabled = false;
1116      UserPod.prototype.reset.call(this, takeFocus);
1117    },
1118
1119    /** @override */
1120    activate: function(e) {
1121      this.expanded = true;
1122      this.focusInput();
1123      return true;
1124    },
1125
1126    /** @override */
1127    handleClickOnPod_: function(e) {
1128      if (this.parentNode.disabled)
1129        return;
1130
1131      this.parentNode.focusPod(this);
1132      this.parentNode.setActivatedPod(this, e);
1133      // Prevent default so that we don't trigger 'focus' event.
1134      e.preventDefault();
1135    },
1136
1137    /**
1138     * Handle mouse and keyboard events for the learn more button. Triggering
1139     * the button causes information about public sessions to be shown.
1140     * @param {Event} event Mouse or keyboard event.
1141     */
1142    handleLearnMoreEvent: function(event) {
1143      switch (event.type) {
1144        // Show informaton on left click. Let any other clicks propagate.
1145        case 'click':
1146          if (event.button != 0)
1147            return;
1148          break;
1149        // Show informaton when <Return> or <Space> is pressed. Let any other
1150        // key presses propagate.
1151        case 'keydown':
1152          switch (event.keyCode) {
1153            case 13:  // Return.
1154            case 32:  // Space.
1155              break;
1156            default:
1157              return;
1158          }
1159          break;
1160      }
1161      chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]);
1162      stopEventPropagation(event);
1163    },
1164  };
1165
1166  /**
1167   * Creates a user pod to be used only in desktop chrome.
1168   * @constructor
1169   * @extends {UserPod}
1170   */
1171  var DesktopUserPod = cr.ui.define(function() {
1172    // Don't just instantiate a UserPod(), as this will call decorate() on the
1173    // parent object, and add duplicate event listeners.
1174    var node = $('user-pod-template').cloneNode(true);
1175    node.removeAttribute('id');
1176    return node;
1177  });
1178
1179  DesktopUserPod.prototype = {
1180    __proto__: UserPod.prototype,
1181
1182    /** @override */
1183    get mainInput() {
1184      if (!this.passwordElement.hidden)
1185        return this.passwordElement;
1186      else
1187        return this.nameElement;
1188    },
1189
1190    /** @override */
1191    decorate: function() {
1192      UserPod.prototype.decorate.call(this);
1193    },
1194
1195    /** @override */
1196    update: function() {
1197      this.imageElement.src = this.user.userImage;
1198      this.nameElement.textContent = this.user.displayName;
1199
1200      var isLockedUser = this.user.needsSignin;
1201      var isSupervisedUser = this.user.locallyManagedUser;
1202      this.signinButtonElement.hidden = true;
1203      this.lockedIndicatorElement.hidden = !isLockedUser;
1204      this.supervisedUserIndicatorElement.hidden = !isSupervisedUser;
1205      this.passwordElement.hidden = !isLockedUser;
1206      this.nameElement.hidden = isLockedUser;
1207
1208      if (this.isAuthTypeUserClick)
1209        this.passwordLabelElement.textContent = this.authValue;
1210
1211      this.actionBoxRemoveUserWarningTextElement.hidden = isSupervisedUser;
1212      this.actionBoxRemoveSupervisedUserWarningTextElement.hidden =
1213          !isSupervisedUser;
1214
1215      UserPod.prototype.updateActionBoxArea.call(this);
1216    },
1217
1218    /** @override */
1219    focusInput: function() {
1220      // For focused pods, display the name unless the pod is locked.
1221      var isLockedUser = this.user.needsSignin;
1222      var isSupervisedUser = this.user.locallyManagedUser;
1223      this.signinButtonElement.hidden = true;
1224      this.lockedIndicatorElement.hidden = !isLockedUser;
1225      this.supervisedUserIndicatorElement.hidden = !isSupervisedUser;
1226      this.passwordElement.hidden = !isLockedUser;
1227      this.nameElement.hidden = isLockedUser;
1228
1229      // Move tabIndex from the whole pod to the main input.
1230      this.tabIndex = -1;
1231      this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1232      this.mainInput.focus();
1233    },
1234
1235    /** @override */
1236    reset: function(takeFocus) {
1237      // Always display the user's name for unfocused pods.
1238      if (!takeFocus)
1239        this.nameElement.hidden = false;
1240      UserPod.prototype.reset.call(this, takeFocus);
1241    },
1242
1243    /** @override */
1244    activate: function(e) {
1245      if (this.passwordElement.hidden) {
1246        Oobe.launchUser(this.user.emailAddress, this.user.displayName);
1247      } else if (!this.passwordElement.value) {
1248        return false;
1249      } else {
1250        chrome.send('authenticatedLaunchUser',
1251                    [this.user.emailAddress,
1252                     this.user.displayName,
1253                     this.passwordElement.value]);
1254      }
1255      this.passwordElement.value = '';
1256      return true;
1257    },
1258
1259    /** @override */
1260    handleClickOnPod_: function(e) {
1261      if (this.parentNode.disabled)
1262        return;
1263
1264      Oobe.clearErrors();
1265      this.parentNode.lastFocusedPod_ = this;
1266
1267      // If this is an unlocked pod, then open a browser window. Otherwise
1268      // just activate the pod and show the password field.
1269      if (!this.user.needsSignin && !this.isActionBoxMenuActive)
1270        this.activate(e);
1271
1272      if (this.isAuthTypeUserClick)
1273        chrome.send('attemptUnlock', [this.user.emailAddress]);
1274    },
1275
1276    /** @override */
1277    handleRemoveUserConfirmationClick_: function(e) {
1278      chrome.send('removeUser', [this.user.profilePath]);
1279    },
1280  };
1281
1282  /**
1283   * Creates a user pod that represents kiosk app.
1284   * @constructor
1285   * @extends {UserPod}
1286   */
1287  var KioskAppPod = cr.ui.define(function() {
1288    var node = UserPod();
1289    return node;
1290  });
1291
1292  KioskAppPod.prototype = {
1293    __proto__: UserPod.prototype,
1294
1295    /** @override */
1296    decorate: function() {
1297      UserPod.prototype.decorate.call(this);
1298      this.launchAppButtonElement.addEventListener('click',
1299                                                   this.activate.bind(this));
1300    },
1301
1302    /** @override */
1303    update: function() {
1304      this.imageElement.src = this.user.iconUrl;
1305      if (this.user.iconHeight && this.user.iconWidth) {
1306        this.imageElement.style.height = this.user.iconHeight;
1307        this.imageElement.style.width = this.user.iconWidth;
1308      }
1309      this.imageElement.alt = this.user.label;
1310      this.imageElement.title = this.user.label;
1311      this.passwordElement.hidden = true;
1312      this.signinButtonElement.hidden = true;
1313      this.launchAppButtonElement.hidden = false;
1314      this.signedInIndicatorElement.hidden = true;
1315      this.nameElement.textContent = this.user.label;
1316
1317      UserPod.prototype.updateActionBoxArea.call(this);
1318      UserPod.prototype.customizeUserPodPerUserType.call(this);
1319    },
1320
1321    /** @override */
1322    get mainInput() {
1323      return this.launchAppButtonElement;
1324    },
1325
1326    /** @override */
1327    focusInput: function() {
1328      this.signinButtonElement.hidden = true;
1329      this.launchAppButtonElement.hidden = false;
1330      this.passwordElement.hidden = true;
1331
1332      // Move tabIndex from the whole pod to the main input.
1333      this.tabIndex = -1;
1334      this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1335      this.mainInput.focus();
1336    },
1337
1338    /** @override */
1339    get forceOnlineSignin() {
1340      return false;
1341    },
1342
1343    /** @override */
1344    activate: function(e) {
1345      var diagnosticMode = e && e.ctrlKey;
1346      this.launchApp_(this.user, diagnosticMode);
1347      return true;
1348    },
1349
1350    /** @override */
1351    handleClickOnPod_: function(e) {
1352      if (this.parentNode.disabled)
1353        return;
1354
1355      Oobe.clearErrors();
1356      this.parentNode.lastFocusedPod_ = this;
1357      this.activate(e);
1358    },
1359
1360    /**
1361     * Launch the app. If |diagnosticMode| is true, ask user to confirm.
1362     * @param {Object} app App data.
1363     * @param {boolean} diagnosticMode Whether to run the app in diagnostic
1364     *     mode.
1365     */
1366    launchApp_: function(app, diagnosticMode) {
1367      if (!diagnosticMode) {
1368        chrome.send('launchKioskApp', [app.id, false]);
1369        return;
1370      }
1371
1372      var oobe = $('oobe');
1373      if (!oobe.confirmDiagnosticMode_) {
1374        oobe.confirmDiagnosticMode_ =
1375            new cr.ui.dialogs.ConfirmDialog(document.body);
1376        oobe.confirmDiagnosticMode_.setOkLabel(
1377            loadTimeData.getString('confirmKioskAppDiagnosticModeYes'));
1378        oobe.confirmDiagnosticMode_.setCancelLabel(
1379            loadTimeData.getString('confirmKioskAppDiagnosticModeNo'));
1380      }
1381
1382      oobe.confirmDiagnosticMode_.show(
1383          loadTimeData.getStringF('confirmKioskAppDiagnosticModeFormat',
1384                                  app.label),
1385          function() {
1386            chrome.send('launchKioskApp', [app.id, true]);
1387          });
1388    },
1389  };
1390
1391  /**
1392   * Creates a new pod row element.
1393   * @constructor
1394   * @extends {HTMLDivElement}
1395   */
1396  var PodRow = cr.ui.define('podrow');
1397
1398  PodRow.prototype = {
1399    __proto__: HTMLDivElement.prototype,
1400
1401    // Whether this user pod row is shown for the first time.
1402    firstShown_: true,
1403
1404    // True if inside focusPod().
1405    insideFocusPod_: false,
1406
1407    // Focused pod.
1408    focusedPod_: undefined,
1409
1410    // Activated pod, i.e. the pod of current login attempt.
1411    activatedPod_: undefined,
1412
1413    // Pod that was most recently focused, if any.
1414    lastFocusedPod_: undefined,
1415
1416    // Pods whose initial images haven't been loaded yet.
1417    podsWithPendingImages_: [],
1418
1419    // Whether pod creation is animated.
1420    userAddIsAnimated_: false,
1421
1422    // Whether pod placement has been postponed.
1423    podPlacementPostponed_: false,
1424
1425    // Standard user pod height/width.
1426    userPodHeight_: 0,
1427    userPodWidth_: 0,
1428
1429    // Array of apps that are shown in addition to other user pods.
1430    apps_: [],
1431
1432    // True to show app pods along with user pods.
1433    shouldShowApps_: true,
1434
1435    // Array of users that are shown (public/supervised/regular).
1436    users_: [],
1437
1438    /** @override */
1439    decorate: function() {
1440      // Event listeners that are installed for the time period during which
1441      // the element is visible.
1442      this.listeners_ = {
1443        focus: [this.handleFocus_.bind(this), true /* useCapture */],
1444        click: [this.handleClick_.bind(this), true],
1445        mousemove: [this.handleMouseMove_.bind(this), false],
1446        keydown: [this.handleKeyDown.bind(this), false]
1447      };
1448
1449      var isDesktopUserManager = Oobe.getInstance().displayType ==
1450          DISPLAY_TYPE.DESKTOP_USER_MANAGER;
1451      this.userPodHeight_ = isDesktopUserManager ? DESKTOP_POD_HEIGHT :
1452                                                   CROS_POD_HEIGHT;
1453      // Same for Chrome OS and desktop.
1454      this.userPodWidth_ = POD_WIDTH;
1455    },
1456
1457    /**
1458     * Returns all the pods in this pod row.
1459     * @type {NodeList}
1460     */
1461    get pods() {
1462      return Array.prototype.slice.call(this.children);
1463    },
1464
1465    /**
1466     * Return true if user pod row has only single user pod in it, which should
1467     * always be focused.
1468     * @type {boolean}
1469     */
1470    get alwaysFocusSinglePod() {
1471      var isDesktopUserManager = Oobe.getInstance().displayType ==
1472          DISPLAY_TYPE.DESKTOP_USER_MANAGER;
1473
1474      return isDesktopUserManager ? false : this.children.length == 1;
1475    },
1476
1477    /**
1478     * Returns pod with the given app id.
1479     * @param {!string} app_id Application id to be matched.
1480     * @return {Object} Pod with the given app id. null if pod hasn't been
1481     *     found.
1482     */
1483    getPodWithAppId_: function(app_id) {
1484      for (var i = 0, pod; pod = this.pods[i]; ++i) {
1485        if (pod.user.isApp && pod.user.id == app_id)
1486          return pod;
1487      }
1488      return null;
1489    },
1490
1491    /**
1492     * Returns pod with the given username (null if there is no such pod).
1493     * @param {string} username Username to be matched.
1494     * @return {Object} Pod with the given username. null if pod hasn't been
1495     *     found.
1496     */
1497    getPodWithUsername_: function(username) {
1498      for (var i = 0, pod; pod = this.pods[i]; ++i) {
1499        if (pod.user.username == username)
1500          return pod;
1501      }
1502      return null;
1503    },
1504
1505    /**
1506     * True if the the pod row is disabled (handles no user interaction).
1507     * @type {boolean}
1508     */
1509    disabled_: false,
1510    get disabled() {
1511      return this.disabled_;
1512    },
1513    set disabled(value) {
1514      this.disabled_ = value;
1515      var controls = this.querySelectorAll('button,input');
1516      for (var i = 0, control; control = controls[i]; ++i) {
1517        control.disabled = value;
1518      }
1519    },
1520
1521    /**
1522     * Creates a user pod from given email.
1523     * @param {!Object} user User info dictionary.
1524     */
1525    createUserPod: function(user) {
1526      var userPod;
1527      if (user.isDesktopUser)
1528        userPod = new DesktopUserPod({user: user});
1529      else if (user.publicAccount)
1530        userPod = new PublicAccountUserPod({user: user});
1531      else if (user.isApp)
1532        userPod = new KioskAppPod({user: user});
1533      else
1534        userPod = new UserPod({user: user});
1535
1536      userPod.hidden = false;
1537      return userPod;
1538    },
1539
1540    /**
1541     * Add an existing user pod to this pod row.
1542     * @param {!Object} user User info dictionary.
1543     * @param {boolean} animated Whether to use init animation.
1544     */
1545    addUserPod: function(user, animated) {
1546      var userPod = this.createUserPod(user);
1547      if (animated) {
1548        userPod.classList.add('init');
1549        userPod.nameElement.classList.add('init');
1550      }
1551
1552      this.appendChild(userPod);
1553      userPod.initialize();
1554    },
1555
1556    /**
1557     * Runs app with a given id from the list of loaded apps.
1558     * @param {!string} app_id of an app to run.
1559     * @param {boolean=} opt_diagnostic_mode Whether to run the app in
1560     *     diagnostic mode. Default is false.
1561     */
1562    findAndRunAppForTesting: function(app_id, opt_diagnostic_mode) {
1563      var app = this.getPodWithAppId_(app_id);
1564      if (app) {
1565        var activationEvent = cr.doc.createEvent('MouseEvents');
1566        var ctrlKey = opt_diagnostic_mode;
1567        activationEvent.initMouseEvent('click', true, true, null,
1568            0, 0, 0, 0, 0, ctrlKey, false, false, false, 0, null);
1569        app.dispatchEvent(activationEvent);
1570      }
1571    },
1572
1573    /**
1574     * Removes user pod from pod row.
1575     * @param {string} email User's email.
1576     */
1577    removeUserPod: function(username) {
1578      var podToRemove = this.getPodWithUsername_(username);
1579      if (podToRemove == null) {
1580        console.warn('Attempt to remove not existing pod for ' + username +
1581            '.');
1582        return;
1583      }
1584      this.removeChild(podToRemove);
1585      if (this.pods.length > 0)
1586        this.placePods_();
1587    },
1588
1589    /**
1590     * Returns index of given pod or -1 if not found.
1591     * @param {UserPod} pod Pod to look up.
1592     * @private
1593     */
1594    indexOf_: function(pod) {
1595      for (var i = 0; i < this.pods.length; ++i) {
1596        if (pod == this.pods[i])
1597          return i;
1598      }
1599      return -1;
1600    },
1601
1602    /**
1603     * Start first time show animation.
1604     */
1605    startInitAnimation: function() {
1606      // Schedule init animation.
1607      for (var i = 0, pod; pod = this.pods[i]; ++i) {
1608        window.setTimeout(removeClass, 500 + i * 70, pod, 'init');
1609        window.setTimeout(removeClass, 700 + i * 70, pod.nameElement, 'init');
1610      }
1611    },
1612
1613    /**
1614     * Start login success animation.
1615     */
1616    startAuthenticatedAnimation: function() {
1617      var activated = this.indexOf_(this.activatedPod_);
1618      if (activated == -1)
1619        return;
1620
1621      for (var i = 0, pod; pod = this.pods[i]; ++i) {
1622        if (i < activated)
1623          pod.classList.add('left');
1624        else if (i > activated)
1625          pod.classList.add('right');
1626        else
1627          pod.classList.add('zoom');
1628      }
1629    },
1630
1631    /**
1632     * Populates pod row with given existing users and start init animation.
1633     * @param {array} users Array of existing user emails.
1634     * @param {boolean} animated Whether to use init animation.
1635     */
1636    loadPods: function(users, animated) {
1637      this.users_ = users;
1638      this.userAddIsAnimated_ = animated;
1639
1640      this.rebuildPods();
1641    },
1642
1643    /**
1644     * Scrolls focused user pod into view.
1645     */
1646    scrollFocusedPodIntoView: function() {
1647      var pod = this.focusedPod_;
1648      if (!pod)
1649        return;
1650
1651      // First check whether focused pod is already fully visible.
1652      var visibleArea = $('scroll-container');
1653      var scrollTop = visibleArea.scrollTop;
1654      var clientHeight = visibleArea.clientHeight;
1655      var podTop = $('oobe').offsetTop + pod.offsetTop;
1656      var padding = USER_POD_KEYBOARD_MIN_PADDING;
1657      if (podTop + pod.height + padding <= scrollTop + clientHeight &&
1658          podTop - padding >= scrollTop) {
1659        return;
1660      }
1661
1662      // Scroll so that user pod is as centered as possible.
1663      visibleArea.scrollTop = podTop - (clientHeight - pod.offsetHeight) / 2;
1664    },
1665
1666    /**
1667     * Rebuilds pod row using users_ and apps_ that were previously set or
1668     * updated.
1669     */
1670    rebuildPods: function() {
1671      var emptyPodRow = this.pods.length == 0;
1672
1673      // Clear existing pods.
1674      this.innerHTML = '';
1675      this.focusedPod_ = undefined;
1676      this.activatedPod_ = undefined;
1677      this.lastFocusedPod_ = undefined;
1678
1679      // Switch off animation
1680      Oobe.getInstance().toggleClass('flying-pods', false);
1681
1682      // Populate the pod row.
1683      for (var i = 0; i < this.users_.length; ++i)
1684        this.addUserPod(this.users_[i], this.userAddIsAnimated_);
1685
1686      for (var i = 0, pod; pod = this.pods[i]; ++i)
1687        this.podsWithPendingImages_.push(pod);
1688
1689      // TODO(nkostylev): Edge case handling when kiosk apps are not fitting.
1690      if (this.shouldShowApps_) {
1691        for (var i = 0; i < this.apps_.length; ++i)
1692          this.addUserPod(this.apps_[i], this.userAddIsAnimated_);
1693      }
1694
1695      // Make sure we eventually show the pod row, even if some image is stuck.
1696      setTimeout(function() {
1697        $('pod-row').classList.remove('images-loading');
1698      }, POD_ROW_IMAGES_LOAD_TIMEOUT_MS);
1699
1700      var isCrosAccountPicker = $('login-header-bar').signinUIState ==
1701          SIGNIN_UI_STATE.ACCOUNT_PICKER;
1702      var isDesktopUserManager = Oobe.getInstance().displayType ==
1703          DISPLAY_TYPE.DESKTOP_USER_MANAGER;
1704
1705      // Chrome OS: immediately recalculate pods layout only when current UI
1706      //            is account picker. Otherwise postpone it.
1707      // Desktop: recalculate pods layout right away.
1708      if (isDesktopUserManager || isCrosAccountPicker) {
1709        this.placePods_();
1710
1711        // Without timeout changes in pods positions will be animated even
1712        // though it happened when 'flying-pods' class was disabled.
1713        setTimeout(function() {
1714          Oobe.getInstance().toggleClass('flying-pods', true);
1715        }, 0);
1716
1717        // On desktop, don't pre-select a pod if it's the only one.
1718        if (isDesktopUserManager && this.pods.length == 1)
1719          this.focusPod();
1720        else
1721          this.focusPod(this.preselectedPod);
1722      } else {
1723        this.podPlacementPostponed_ = true;
1724
1725        // Update [Cancel] button state.
1726        if ($('login-header-bar').signinUIState ==
1727                SIGNIN_UI_STATE.GAIA_SIGNIN &&
1728            emptyPodRow &&
1729            this.pods.length > 0) {
1730          login.GaiaSigninScreen.updateCancelButtonState();
1731        }
1732      }
1733    },
1734
1735    /**
1736     * Adds given apps to the pod row.
1737     * @param {array} apps Array of apps.
1738     */
1739    setApps: function(apps) {
1740      this.apps_ = apps;
1741      this.rebuildPods();
1742      chrome.send('kioskAppsLoaded');
1743
1744      // Check whether there's a pending kiosk app error.
1745      window.setTimeout(function() {
1746        chrome.send('checkKioskAppLaunchError');
1747      }, 500);
1748    },
1749
1750    /**
1751     * Sets whether should show app pods.
1752     * @param {boolean} shouldShowApps Whether app pods should be shown.
1753     */
1754    setShouldShowApps: function(shouldShowApps) {
1755      if (this.shouldShowApps_ == shouldShowApps)
1756        return;
1757
1758      this.shouldShowApps_ = shouldShowApps;
1759      this.rebuildPods();
1760    },
1761
1762    /**
1763     * Shows a custom icon on a user pod besides the input field.
1764     * @param {string} username Username of pod to add button
1765     * @param {{scale1x: string, scale2x: string}} icon Dictionary of URLs of
1766     *     the custom icon's representations for 1x and 2x scale factors.
1767     */
1768    showUserPodCustomIcon: function(username, icon) {
1769      var pod = this.getPodWithUsername_(username);
1770      if (pod == null) {
1771        console.error('Unable to show user pod button for ' + username +
1772                      ': user pod not found.');
1773        return;
1774      }
1775
1776      pod.customIconElement.hidden = false;
1777      pod.customIconElement.style.backgroundImage =
1778          '-webkit-image-set(' +
1779              'url(' + icon.scale1x + ') 1x,' +
1780              'url(' + icon.scale2x + ') 2x)';
1781    },
1782
1783    /**
1784     * Hides the custom icon in the user pod added by showUserPodCustomIcon().
1785     * @param {string} username Username of pod to remove button
1786     */
1787    hideUserPodCustomIcon: function(username) {
1788      var pod = this.getPodWithUsername_(username);
1789      if (pod == null) {
1790        console.error('Unable to hide user pod button for ' + username +
1791                      ': user pod not found.');
1792        return;
1793      }
1794
1795      pod.customIconElement.hidden = true;
1796    },
1797
1798    /**
1799     * Sets the authentication type used to authenticate the user.
1800     * @param {string} username Username of selected user
1801     * @param {number} authType Authentication type, must be one of the
1802     *                          values listed in AUTH_TYPE enum.
1803     * @param {string} value The initial value to use for authentication.
1804     */
1805    setAuthType: function(username, authType, value) {
1806      var pod = this.getPodWithUsername_(username);
1807      if (pod == null) {
1808        console.error('Unable to set auth type for ' + username +
1809                      ': user pod not found.');
1810        return;
1811      }
1812      pod.setAuthType(authType, value);
1813    },
1814
1815    /**
1816     * Shows a tooltip bubble explaining Easy Unlock for the focused pod.
1817     */
1818    showEasyUnlockBubble: function() {
1819      if (!this.focusedPod_) {
1820        console.error('No focused pod to show Easy Unlock bubble.');
1821        return;
1822      }
1823
1824      var bubbleContent = document.createElement('div');
1825      bubbleContent.classList.add('easy-unlock-button-content');
1826      bubbleContent.textContent = loadTimeData.getString('easyUnlockTooltip');
1827
1828      var attachElement = this.focusedPod_.customIconElement;
1829      /** @const */ var BUBBLE_OFFSET = 20;
1830      /** @const */ var BUBBLE_PADDING = 8;
1831      $('bubble').showContentForElement(attachElement,
1832                                        cr.ui.Bubble.Attachment.RIGHT,
1833                                        bubbleContent,
1834                                        BUBBLE_OFFSET,
1835                                        BUBBLE_PADDING);
1836    },
1837
1838    /**
1839     * Called when window was resized.
1840     */
1841    onWindowResize: function() {
1842      var layout = this.calculateLayout_();
1843      if (layout.columns != this.columns || layout.rows != this.rows)
1844        this.placePods_();
1845
1846      if (Oobe.getInstance().virtualKeyboardShown)
1847        this.scrollFocusedPodIntoView();
1848    },
1849
1850    /**
1851     * Returns width of podrow having |columns| number of columns.
1852     * @private
1853     */
1854    columnsToWidth_: function(columns) {
1855      var isDesktopUserManager = Oobe.getInstance().displayType ==
1856          DISPLAY_TYPE.DESKTOP_USER_MANAGER;
1857      var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] :
1858                                          MARGIN_BY_COLUMNS[columns];
1859      var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
1860                                              POD_ROW_PADDING;
1861      return 2 * rowPadding + columns * this.userPodWidth_ +
1862          (columns - 1) * margin;
1863    },
1864
1865    /**
1866     * Returns height of podrow having |rows| number of rows.
1867     * @private
1868     */
1869    rowsToHeight_: function(rows) {
1870      var isDesktopUserManager = Oobe.getInstance().displayType ==
1871          DISPLAY_TYPE.DESKTOP_USER_MANAGER;
1872      var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
1873                                              POD_ROW_PADDING;
1874      return 2 * rowPadding + rows * this.userPodHeight_;
1875    },
1876
1877    /**
1878     * Calculates number of columns and rows that podrow should have in order to
1879     * hold as much its pods as possible for current screen size. Also it tries
1880     * to choose layout that looks good.
1881     * @return {{columns: number, rows: number}}
1882     */
1883    calculateLayout_: function() {
1884      var preferredColumns = this.pods.length < COLUMNS.length ?
1885          COLUMNS[this.pods.length] : COLUMNS[COLUMNS.length - 1];
1886      var maxWidth = Oobe.getInstance().clientAreaSize.width;
1887      var columns = preferredColumns;
1888      while (maxWidth < this.columnsToWidth_(columns) && columns > 1)
1889        --columns;
1890      var rows = Math.floor((this.pods.length - 1) / columns) + 1;
1891      if (getComputedStyle(
1892          $('signin-banner'), null).getPropertyValue('display') != 'none') {
1893        rows = Math.min(rows, MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER);
1894      }
1895      var maxHeigth = Oobe.getInstance().clientAreaSize.height;
1896      while (maxHeigth < this.rowsToHeight_(rows) && rows > 1)
1897        --rows;
1898      // One more iteration if it's not enough cells to place all pods.
1899      while (maxWidth >= this.columnsToWidth_(columns + 1) &&
1900             columns * rows < this.pods.length &&
1901             columns < MAX_NUMBER_OF_COLUMNS) {
1902         ++columns;
1903      }
1904      return {columns: columns, rows: rows};
1905    },
1906
1907    /**
1908     * Places pods onto their positions onto pod grid.
1909     * @private
1910     */
1911    placePods_: function() {
1912      var layout = this.calculateLayout_();
1913      var columns = this.columns = layout.columns;
1914      var rows = this.rows = layout.rows;
1915      var maxPodsNumber = columns * rows;
1916      var isDesktopUserManager = Oobe.getInstance().displayType ==
1917          DISPLAY_TYPE.DESKTOP_USER_MANAGER;
1918      var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] :
1919                                          MARGIN_BY_COLUMNS[columns];
1920      this.parentNode.setPreferredSize(
1921          this.columnsToWidth_(columns), this.rowsToHeight_(rows));
1922      var height = this.userPodHeight_;
1923      var width = this.userPodWidth_;
1924      this.pods.forEach(function(pod, index) {
1925        if (pod.offsetHeight != height) {
1926          console.error('Pod offsetHeight (' + pod.offsetHeight +
1927              ') and POD_HEIGHT (' + height + ') are not equal.');
1928        }
1929        if (pod.offsetWidth != width) {
1930          console.error('Pod offsetWidth (' + pod.offsetWidth +
1931              ') and POD_WIDTH (' + width + ') are not equal.');
1932        }
1933        if (index >= maxPodsNumber) {
1934           pod.hidden = true;
1935           return;
1936        }
1937        pod.hidden = false;
1938        var column = index % columns;
1939        var row = Math.floor(index / columns);
1940        var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
1941                                                POD_ROW_PADDING;
1942        pod.left = rowPadding + column * (width + margin);
1943
1944        // On desktop, we want the rows to always be equally spaced.
1945        pod.top = isDesktopUserManager ? row * (height + rowPadding) :
1946                                         row * height + rowPadding;
1947      });
1948      Oobe.getInstance().updateScreenSize(this.parentNode);
1949    },
1950
1951    /**
1952     * Number of columns.
1953     * @type {?number}
1954     */
1955    set columns(columns) {
1956      // Cannot use 'columns' here.
1957      this.setAttribute('ncolumns', columns);
1958    },
1959    get columns() {
1960      return parseInt(this.getAttribute('ncolumns'));
1961    },
1962
1963    /**
1964     * Number of rows.
1965     * @type {?number}
1966     */
1967    set rows(rows) {
1968      // Cannot use 'rows' here.
1969      this.setAttribute('nrows', rows);
1970    },
1971    get rows() {
1972      return parseInt(this.getAttribute('nrows'));
1973    },
1974
1975    /**
1976     * Whether the pod is currently focused.
1977     * @param {UserPod} pod Pod to check for focus.
1978     * @return {boolean} Pod focus status.
1979     */
1980    isFocused: function(pod) {
1981      return this.focusedPod_ == pod;
1982    },
1983
1984    /**
1985     * Focuses a given user pod or clear focus when given null.
1986     * @param {UserPod=} podToFocus User pod to focus (undefined clears focus).
1987     * @param {boolean=} opt_force If true, forces focus update even when
1988     *     podToFocus is already focused.
1989     */
1990    focusPod: function(podToFocus, opt_force) {
1991      if (this.isFocused(podToFocus) && !opt_force) {
1992        // Calling focusPod w/o podToFocus means reset.
1993        if (!podToFocus)
1994          Oobe.clearErrors();
1995        this.keyboardActivated_ = false;
1996        return;
1997      }
1998
1999      // Make sure that we don't focus pods that are not allowed to be focused.
2000      // TODO(nkostylev): Fix various keyboard focus related issues caused
2001      // by this approach. http://crbug.com/339042
2002      if (podToFocus && podToFocus.classList.contains('not-focusable')) {
2003        this.keyboardActivated_ = false;
2004        return;
2005      }
2006
2007      // Make sure there's only one focusPod operation happening at a time.
2008      if (this.insideFocusPod_) {
2009        this.keyboardActivated_ = false;
2010        return;
2011      }
2012      this.insideFocusPod_ = true;
2013
2014      for (var i = 0, pod; pod = this.pods[i]; ++i) {
2015        if (!this.alwaysFocusSinglePod) {
2016          pod.isActionBoxMenuActive = false;
2017        }
2018        if (pod != podToFocus) {
2019          pod.isActionBoxMenuHovered = false;
2020          pod.classList.remove('focused');
2021          // On Desktop, the faded style is not set correctly, so we should
2022          // manually fade out non-focused pods.
2023          if (pod.user.isDesktopUser)
2024            pod.classList.add('faded');
2025          else
2026            pod.classList.remove('faded');
2027          pod.reset(false);
2028        }
2029      }
2030
2031      // Clear any error messages for previous pod.
2032      if (!this.isFocused(podToFocus))
2033        Oobe.clearErrors();
2034
2035      var hadFocus = !!this.focusedPod_;
2036      this.focusedPod_ = podToFocus;
2037      if (podToFocus) {
2038        podToFocus.classList.remove('faded');
2039        podToFocus.classList.add('focused');
2040        podToFocus.reset(true);  // Reset and give focus.
2041        // focusPod() automatically loads wallpaper
2042        if (!podToFocus.user.isApp)
2043          chrome.send('focusPod', [podToFocus.user.username]);
2044        this.firstShown_ = false;
2045        this.lastFocusedPod_ = podToFocus;
2046
2047        if (Oobe.getInstance().virtualKeyboardShown)
2048          this.scrollFocusedPodIntoView();
2049      }
2050      this.insideFocusPod_ = false;
2051      this.keyboardActivated_ = false;
2052    },
2053
2054    /**
2055     * Focuses a given user pod by index or clear focus when given null.
2056     * @param {int=} podToFocus index of User pod to focus.
2057     * @param {boolean=} opt_force If true, forces focus update even when
2058     *     podToFocus is already focused.
2059     */
2060    focusPodByIndex: function(podToFocus, opt_force) {
2061      if (podToFocus < this.pods.length)
2062        this.focusPod(this.pods[podToFocus], opt_force);
2063    },
2064
2065    /**
2066     * Resets wallpaper to the last active user's wallpaper, if any.
2067     */
2068    loadLastWallpaper: function() {
2069      if (this.lastFocusedPod_ && !this.lastFocusedPod_.user.isApp)
2070        chrome.send('loadWallpaper', [this.lastFocusedPod_.user.username]);
2071    },
2072
2073    /**
2074     * Returns the currently activated pod.
2075     * @type {UserPod}
2076     */
2077    get activatedPod() {
2078      return this.activatedPod_;
2079    },
2080
2081    /**
2082     * Sets currently activated pod.
2083     * @param {UserPod} pod Pod to check for focus.
2084     * @param {Event} e Event object.
2085     */
2086    setActivatedPod: function(pod, e) {
2087      if (pod && pod.activate(e))
2088        this.activatedPod_ = pod;
2089    },
2090
2091    /**
2092     * The pod of the signed-in user, if any; null otherwise.
2093     * @type {?UserPod}
2094     */
2095    get lockedPod() {
2096      for (var i = 0, pod; pod = this.pods[i]; ++i) {
2097        if (pod.user.signedIn)
2098          return pod;
2099      }
2100      return null;
2101    },
2102
2103    /**
2104     * The pod that is preselected on user pod row show.
2105     * @type {?UserPod}
2106     */
2107    get preselectedPod() {
2108      var lockedPod = this.lockedPod;
2109      var preselectedPod = PRESELECT_FIRST_POD ?
2110          lockedPod || this.pods[0] : lockedPod;
2111      return preselectedPod;
2112    },
2113
2114    /**
2115     * Resets input UI.
2116     * @param {boolean} takeFocus True to take focus.
2117     */
2118    reset: function(takeFocus) {
2119      this.disabled = false;
2120      if (this.activatedPod_)
2121        this.activatedPod_.reset(takeFocus);
2122    },
2123
2124    /**
2125     * Restores input focus to current selected pod, if there is any.
2126     */
2127    refocusCurrentPod: function() {
2128      if (this.focusedPod_) {
2129        this.focusedPod_.focusInput();
2130      }
2131    },
2132
2133    /**
2134     * Clears focused pod password field.
2135     */
2136    clearFocusedPod: function() {
2137      if (!this.disabled && this.focusedPod_)
2138        this.focusedPod_.reset(true);
2139    },
2140
2141    /**
2142     * Shows signin UI.
2143     * @param {string} email Email for signin UI.
2144     */
2145    showSigninUI: function(email) {
2146      // Clear any error messages that might still be around.
2147      Oobe.clearErrors();
2148      this.disabled = true;
2149      this.lastFocusedPod_ = this.getPodWithUsername_(email);
2150      Oobe.showSigninUI(email);
2151    },
2152
2153    /**
2154     * Updates current image of a user.
2155     * @param {string} username User for which to update the image.
2156     */
2157    updateUserImage: function(username) {
2158      var pod = this.getPodWithUsername_(username);
2159      if (pod)
2160        pod.updateUserImage();
2161    },
2162
2163    /**
2164     * Handler of click event.
2165     * @param {Event} e Click Event object.
2166     * @private
2167     */
2168    handleClick_: function(e) {
2169      if (this.disabled)
2170        return;
2171
2172      // Clear all menus if the click is outside pod menu and its
2173      // button area.
2174      if (!findAncestorByClass(e.target, 'action-box-menu') &&
2175          !findAncestorByClass(e.target, 'action-box-area')) {
2176        for (var i = 0, pod; pod = this.pods[i]; ++i)
2177          pod.isActionBoxMenuActive = false;
2178      }
2179
2180      // Clears focus if not clicked on a pod and if there's more than one pod.
2181      var pod = findAncestorByClass(e.target, 'pod');
2182      if ((!pod || pod.parentNode != this) && !this.alwaysFocusSinglePod) {
2183        this.focusPod();
2184      }
2185
2186      if (pod)
2187        pod.isActionBoxMenuHovered = true;
2188
2189      // Return focus back to single pod.
2190      if (this.alwaysFocusSinglePod) {
2191        this.focusPod(this.focusedPod_, true /* force */);
2192        if (!pod)
2193          this.focusedPod_.isActionBoxMenuHovered = false;
2194      }
2195    },
2196
2197    /**
2198     * Handler of mouse move event.
2199     * @param {Event} e Click Event object.
2200     * @private
2201     */
2202    handleMouseMove_: function(e) {
2203      if (this.disabled)
2204        return;
2205      if (e.webkitMovementX == 0 && e.webkitMovementY == 0)
2206        return;
2207
2208      // Defocus (thus hide) action box, if it is focused on a user pod
2209      // and the pointer is not hovering over it.
2210      var pod = findAncestorByClass(e.target, 'pod');
2211      if (document.activeElement &&
2212          document.activeElement.parentNode != pod &&
2213          document.activeElement.classList.contains('action-box-area')) {
2214        document.activeElement.parentNode.focus();
2215      }
2216
2217      if (pod)
2218        pod.isActionBoxMenuHovered = true;
2219
2220      // Hide action boxes on other user pods.
2221      for (var i = 0, p; p = this.pods[i]; ++i)
2222        if (p != pod && !p.isActionBoxMenuActive)
2223          p.isActionBoxMenuHovered = false;
2224    },
2225
2226    /**
2227     * Handles focus event.
2228     * @param {Event} e Focus Event object.
2229     * @private
2230     */
2231    handleFocus_: function(e) {
2232      if (this.disabled)
2233        return;
2234      if (e.target.parentNode == this) {
2235        // Focus on a pod
2236        if (e.target.classList.contains('focused'))
2237          e.target.focusInput();
2238        else
2239          this.focusPod(e.target);
2240        return;
2241      }
2242
2243      var pod = findAncestorByClass(e.target, 'pod');
2244      if (pod && pod.parentNode == this) {
2245        // Focus on a control of a pod but not on the action area button.
2246        if (!pod.classList.contains('focused') &&
2247            !e.target.classList.contains('action-box-button')) {
2248          this.focusPod(pod);
2249          e.target.focus();
2250        }
2251        return;
2252      }
2253
2254      // Clears pod focus when we reach here. It means new focus is neither
2255      // on a pod nor on a button/input for a pod.
2256      // Do not "defocus" user pod when it is a single pod.
2257      // That means that 'focused' class will not be removed and
2258      // input field/button will always be visible.
2259      if (!this.alwaysFocusSinglePod)
2260        this.focusPod();
2261    },
2262
2263    /**
2264     * Handler of keydown event.
2265     * @param {Event} e KeyDown Event object.
2266     */
2267    handleKeyDown: function(e) {
2268      if (this.disabled)
2269        return;
2270      var editing = e.target.tagName == 'INPUT' && e.target.value;
2271      switch (e.keyIdentifier) {
2272        case 'Left':
2273          if (!editing) {
2274            this.keyboardActivated_ = true;
2275            if (this.focusedPod_ && this.focusedPod_.previousElementSibling)
2276              this.focusPod(this.focusedPod_.previousElementSibling);
2277            else
2278              this.focusPod(this.lastElementChild);
2279
2280            e.stopPropagation();
2281          }
2282          break;
2283        case 'Right':
2284          if (!editing) {
2285            this.keyboardActivated_ = true;
2286            if (this.focusedPod_ && this.focusedPod_.nextElementSibling)
2287              this.focusPod(this.focusedPod_.nextElementSibling);
2288            else
2289              this.focusPod(this.firstElementChild);
2290
2291            e.stopPropagation();
2292          }
2293          break;
2294        case 'Enter':
2295          if (this.focusedPod_) {
2296            var targetTag = e.target.tagName;
2297            if (e.target == this.focusedPod_.passwordElement ||
2298                (targetTag != 'INPUT' &&
2299                 targetTag != 'BUTTON' &&
2300                 targetTag != 'A')) {
2301              this.setActivatedPod(this.focusedPod_, e);
2302              e.stopPropagation();
2303            }
2304          }
2305          break;
2306        case 'U+001B':  // Esc
2307          if (!this.alwaysFocusSinglePod)
2308            this.focusPod();
2309          break;
2310      }
2311    },
2312
2313    /**
2314     * Called right after the pod row is shown.
2315     */
2316    handleAfterShow: function() {
2317      // Without timeout changes in pods positions will be animated even though
2318      // it happened when 'flying-pods' class was disabled.
2319      setTimeout(function() {
2320        Oobe.getInstance().toggleClass('flying-pods', true);
2321      }, 0);
2322      // Force input focus for user pod on show and once transition ends.
2323      if (this.focusedPod_) {
2324        var focusedPod = this.focusedPod_;
2325        var screen = this.parentNode;
2326        var self = this;
2327        focusedPod.addEventListener('webkitTransitionEnd', function f(e) {
2328          focusedPod.removeEventListener('webkitTransitionEnd', f);
2329          focusedPod.reset(true);
2330          // Notify screen that it is ready.
2331          screen.onShow();
2332        });
2333        // Guard timer for 1 second -- it would conver all possible animations.
2334        ensureTransitionEndEvent(focusedPod, 1000);
2335      }
2336    },
2337
2338    /**
2339     * Called right before the pod row is shown.
2340     */
2341    handleBeforeShow: function() {
2342      Oobe.getInstance().toggleClass('flying-pods', false);
2343      for (var event in this.listeners_) {
2344        this.ownerDocument.addEventListener(
2345            event, this.listeners_[event][0], this.listeners_[event][1]);
2346      }
2347      $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR;
2348
2349      if (this.podPlacementPostponed_) {
2350        this.podPlacementPostponed_ = false;
2351        this.placePods_();
2352        this.focusPod(this.preselectedPod);
2353      }
2354    },
2355
2356    /**
2357     * Called when the element is hidden.
2358     */
2359    handleHide: function() {
2360      for (var event in this.listeners_) {
2361        this.ownerDocument.removeEventListener(
2362            event, this.listeners_[event][0], this.listeners_[event][1]);
2363      }
2364      $('login-header-bar').buttonsTabIndex = 0;
2365    },
2366
2367    /**
2368     * Called when a pod's user image finishes loading.
2369     */
2370    handlePodImageLoad: function(pod) {
2371      var index = this.podsWithPendingImages_.indexOf(pod);
2372      if (index == -1) {
2373        return;
2374      }
2375
2376      this.podsWithPendingImages_.splice(index, 1);
2377      if (this.podsWithPendingImages_.length == 0) {
2378        this.classList.remove('images-loading');
2379      }
2380    }
2381  };
2382
2383  return {
2384    PodRow: PodRow
2385  };
2386});
2387