text: // Copyright 2007 The Closure Library Authors. All Rights Reserved.
  //
  // Licensed under the Apache License, Version 2.0 (the "License");
  // you may not use this file except in compliance with the License.
  // You may obtain a copy of the License at
  //
  //      http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an "AS-IS" BASIS,
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  // See the License for the specific language governing permissions and
  // limitations under the License.
  
  /**
   * @fileoverview Base class for containers that host {@link goog.ui.Control}s,
   * such as menus and toolbars.  Provides default keyboard and mouse event
   * handling and child management, based on a generalized version of
   * {@link goog.ui.Menu}.
   *
   * @author attila@google.com (Attila Bodis)
   * @see ../demos/container.html
   */
  // TODO(attila):  Fix code/logic duplication between this and goog.ui.Control.
  // TODO(attila):  Maybe pull common stuff all the way up into Component...?
  
  goog.provide('goog.ui.Container');
  goog.provide('goog.ui.Container.EventType');
  goog.provide('goog.ui.Container.Orientation');
  
  goog.require('goog.a11y.aria');
  goog.require('goog.a11y.aria.State');
  goog.require('goog.asserts');
  goog.require('goog.dom');
  goog.require('goog.events.EventType');
  goog.require('goog.events.KeyCodes');
  goog.require('goog.events.KeyHandler');
  goog.require('goog.object');
  goog.require('goog.style');
  goog.require('goog.ui.Component');
  goog.require('goog.ui.ComponentUtil');
  goog.require('goog.ui.ContainerRenderer');
  goog.require('goog.ui.Control');
  
  
  
  /**
   * Base class for containers.  Extends {@link goog.ui.Component} by adding
   * the following:
   *  <ul>
   *    <li>a {@link goog.events.KeyHandler}, to simplify keyboard handling,
   *    <li>a pluggable <em>renderer</em> framework, to simplify the creation of
   *        containers without the need to subclass this class,
   *    <li>methods to manage child controls hosted in the container,
   *    <li>default mouse and keyboard event handling methods.
   *  </ul>
   * @param {?goog.ui.Container.Orientation=} opt_orientation Container
   *     orientation; defaults to `VERTICAL`.
   * @param {goog.ui.ContainerRenderer=} opt_renderer Renderer used to render or
   *     decorate the container; defaults to {@link goog.ui.ContainerRenderer}.
   * @param {goog.dom.DomHelper=} opt_domHelper DOM helper, used for document
   *     interaction.
   * @extends {goog.ui.Component}
   * @constructor
   */
  goog.ui.Container = function(opt_orientation, opt_renderer, opt_domHelper) {
    goog.ui.Component.call(this, opt_domHelper);
    this.renderer_ = opt_renderer || goog.ui.ContainerRenderer.getInstance();
    this.orientation_ = opt_orientation || this.renderer_.getDefaultOrientation();
  };
  goog.inherits(goog.ui.Container, goog.ui.Component);
  goog.tagUnsealableClass(goog.ui.Container);
  
  
  /**
   * Container-specific events.
   * @enum {string}
   */
  goog.ui.Container.EventType = {
    /**
     * Dispatched after a goog.ui.Container becomes visible. Non-cancellable.
     * NOTE(user): This event really shouldn't exist, because the
     * goog.ui.Component.EventType.SHOW event should behave like this one. But the
     * SHOW event for containers has been behaving as other components'
     * BEFORE_SHOW event for a long time, and too much code relies on that old
     * behavior to fix it now.
     */
    AFTER_SHOW: 'aftershow',
  
    /**
     * Dispatched after a goog.ui.Container becomes invisible. Non-cancellable.
     */
    AFTER_HIDE: 'afterhide'
  };
  
  
  /**
   * Container orientation constants.
   * @enum {string}
   */
  goog.ui.Container.Orientation = {
    HORIZONTAL: 'horizontal',
    VERTICAL: 'vertical'
  };
  
  
  /**
   * Allows an alternative element to be set to receive key events, otherwise
   * defers to the renderer's element choice.
   * @type {Element|undefined}
   * @private
   */
  goog.ui.Container.prototype.keyEventTarget_ = null;
  
  
  /**
   * Keyboard event handler.
   * @type {goog.events.KeyHandler?}
   * @private
   */
  goog.ui.Container.prototype.keyHandler_ = null;
  
  
  /**
   * Renderer for the container.  Defaults to {@link goog.ui.ContainerRenderer}.
   * @type {goog.ui.ContainerRenderer?}
   * @private
   */
  goog.ui.Container.prototype.renderer_ = null;
  
  
  /**
   * Container orientation; determines layout and default keyboard navigation.
   * @type {?goog.ui.Container.Orientation}
   * @private
   */
  goog.ui.Container.prototype.orientation_ = null;
  
  
  /**
   * Whether the container is set to be visible.  Defaults to true.
   * @type {boolean}
   * @private
   */
  goog.ui.Container.prototype.visible_ = true;
  
  
  /**
   * Whether the container is enabled and reacting to keyboard and mouse events.
   * Defaults to true.
   * @type {boolean}
   * @private
   */
  goog.ui.Container.prototype.enabled_ = true;
  
  
  /**
   * Whether the container supports keyboard focus.  Defaults to true.  Focusable
   * containers have a `tabIndex` and can be navigated to via the keyboard.
   * @type {boolean}
   * @private
   */
  goog.ui.Container.prototype.focusable_ = true;
  
  
  /**
   * The 0-based index of the currently highlighted control in the container
   * (-1 if none).
   * @type {number}
   * @private
   */
  goog.ui.Container.prototype.highlightedIndex_ = -1;
  
  
  /**
   * The currently open (expanded) control in the container (null if none).
   * @type {goog.ui.Control?}
   * @private
   */
  goog.ui.Container.prototype.openItem_ = null;
  
  
  /**
   * Whether the mouse button is held down.  Defaults to false.  This flag is set
   * when the user mouses down over the container, and remains set until they
   * release the mouse button.
   * @type {boolean}
   * @private
   */
  goog.ui.Container.prototype.mouseButtonPressed_ = false;
  
  
  /**
   * Whether focus of child components should be allowed.  Only effective if
   * focusable_ is set to false.
   * @type {boolean}
   * @private
   */
  goog.ui.Container.prototype.allowFocusableChildren_ = false;
  
  
  /**
   * Whether highlighting a child component should also open it.
   * @type {boolean}
   * @private
   */
  goog.ui.Container.prototype.openFollowsHighlight_ = true;
  
  
  /**
   * Map of DOM IDs to child controls.  Each key is the DOM ID of a child
   * control's root element; each value is a reference to the child control
   * itself.  Used for looking up the child control corresponding to a DOM
   * node in O(1) time.
   * @type {Object}
   * @private
   */
  goog.ui.Container.prototype.childElementIdMap_ = null;
  
  
  // Event handler and renderer management.
  
  
  /**
   * Returns the DOM element on which the container is listening for keyboard
   * events (null if none).
   * @return {Element} Element on which the container is listening for key
   *     events.
   */
  goog.ui.Container.prototype.getKeyEventTarget = function() {
    // Delegate to renderer, unless we've set an explicit target.
    return this.keyEventTarget_ || this.renderer_.getKeyEventTarget(this);
  };
  
  
  /**
   * Attaches an element on which to listen for key events.
   * @param {Element|undefined} element The element to attach, or null/undefined
   *     to attach to the default element.
   */
  goog.ui.Container.prototype.setKeyEventTarget = function(element) {
    if (this.focusable_) {
      var oldTarget = this.getKeyEventTarget();
      var inDocument = this.isInDocument();
  
      this.keyEventTarget_ = element;
      var newTarget = this.getKeyEventTarget();
  
      if (inDocument) {
        // Unlisten for events on the old key target.  Requires us to reset
        // key target state temporarily.
        this.keyEventTarget_ = oldTarget;
        this.enableFocusHandling_(false);
        this.keyEventTarget_ = element;
  
        // Listen for events on the new key target.
        this.getKeyHandler().attach(newTarget);
        this.enableFocusHandling_(true);
      }
    } else {
      throw new Error(
          'Can\'t set key event target for container ' +
          'that doesn\'t support keyboard focus!');
    }
  };
  
  
  /**
   * Returns the keyboard event handler for this container, lazily created the
   * first time this method is called.  The keyboard event handler listens for
   * keyboard events on the container's key event target, as determined by its
   * renderer.
   * @return {!goog.events.KeyHandler} Keyboard event handler for this container.
   */
  goog.ui.Container.prototype.getKeyHandler = function() {
    return this.keyHandler_ ||
        (this.keyHandler_ = new goog.events.KeyHandler(this.getKeyEventTarget()));
  };
  
  
  /**
   * Returns the renderer used by this container to render itself or to decorate
   * an existing element.
   * @return {goog.ui.ContainerRenderer} Renderer used by the container.
   */
  goog.ui.Container.prototype.getRenderer = function() {
    return this.renderer_;
  };
  
  
  /**
   * Registers the given renderer with the container.  Changing renderers after
   * the container has already been rendered or decorated is an error.
   * @param {goog.ui.ContainerRenderer} renderer Renderer used by the container.
   */
  goog.ui.Container.prototype.setRenderer = function(renderer) {
    if (this.getElement()) {
      // Too late.
      throw new Error(goog.ui.Component.Error.ALREADY_RENDERED);
    }
  
    this.renderer_ = renderer;
  };
  
  
  // Standard goog.ui.Component implementation.
  
  
  /**
   * Creates the container's DOM.
   * @override
   */
  goog.ui.Container.prototype.createDom = function() {
    // Delegate to renderer.
    this.setElementInternal(this.renderer_.createDom(this));
  };
  
  
  /**
   * Returns the DOM element into which child components are to be rendered,
   * or null if the container itself hasn't been rendered yet.  Overrides
   * {@link goog.ui.Component#getContentElement} by delegating to the renderer.
   * @return {Element} Element to contain child elements (null if none).
   * @override
   */
  goog.ui.Container.prototype.getContentElement = function() {
    // Delegate to renderer.
    return this.renderer_.getContentElement(this.getElement());
  };
  
  
  /**
   * Returns true if the given element can be decorated by this container.
   * Overrides {@link goog.ui.Component#canDecorate}.
   * @param {Element} element Element to decorate.
   * @return {boolean} True iff the element can be decorated.
   * @override
   */
  goog.ui.Container.prototype.canDecorate = function(element) {
    // Delegate to renderer.
    return this.renderer_.canDecorate(element);
  };
  
  
  /**
   * Decorates the given element with this container. Overrides {@link
   * goog.ui.Component#decorateInternal}.  Considered protected.
   * @param {Element} element Element to decorate.
   * @override
   */
  goog.ui.Container.prototype.decorateInternal = function(element) {
    // Delegate to renderer.
    this.setElementInternal(this.renderer_.decorate(this, element));
    // Check whether the decorated element is explicitly styled to be invisible.
    if (element.style.display == 'none') {
      this.visible_ = false;
    }
  };
  
  
  /**
   * Configures the container after its DOM has been rendered, and sets up event
   * handling.  Overrides {@link goog.ui.Component#enterDocument}.
   * @override
   */
  goog.ui.Container.prototype.enterDocument = function() {
    goog.ui.Container.superClass_.enterDocument.call(this);
  
    this.forEachChild(function(child) {
      if (child.isInDocument()) {
        this.registerChildId_(child);
      }
    }, this);
  
    var elem = this.getElement();
  
    // Call the renderer's initializeDom method to initialize the container's DOM.
    this.renderer_.initializeDom(this);
  
    // Initialize visibility (opt_force = true, so we don't dispatch events).
    this.setVisible(this.visible_, true);
  
    var MouseEventType = goog.ui.ComponentUtil.getMouseEventType(this);
  
    // Handle events dispatched by child controls.
    this.getHandler()
        .listen(this, goog.ui.Component.EventType.ENTER, this.handleEnterItem)
        .listen(
            this, goog.ui.Component.EventType.HIGHLIGHT, this.handleHighlightItem)
        .listen(
            this, goog.ui.Component.EventType.UNHIGHLIGHT,
            this.handleUnHighlightItem)
        .listen(this, goog.ui.Component.EventType.OPEN, this.handleOpenItem)
        .listen(this, goog.ui.Component.EventType.CLOSE, this.handleCloseItem)
  
        // Handle mouse events.
        .listen(elem, MouseEventType.MOUSEDOWN, this.handleMouseDown)
        .listen(
            goog.dom.getOwnerDocument(elem),
            [MouseEventType.MOUSEUP, MouseEventType.MOUSECANCEL],
            this.handleDocumentMouseUp)
  
        // Handle mouse events on behalf of controls in the container.
        .listen(
            elem,
            [
              MouseEventType.MOUSEDOWN,
              MouseEventType.MOUSEUP,
              MouseEventType.MOUSECANCEL,
              MouseEventType.MOUSEOVER,
              MouseEventType.MOUSEOUT,
              goog.events.EventType.CONTEXTMENU
            ],
            this.handleChildMouseEvents);
  
    if (this.pointerEventsEnabled()) {
      // Prevent pointer events from capturing the target element so they behave
      // more like mouse events.
      this.getHandler().listen(
          elem, goog.events.EventType.GOTPOINTERCAPTURE,
          this.preventPointerCapture_);
    }
  
    // If the container is focusable, set up keyboard event handling.
    if (this.isFocusable()) {
      this.enableFocusHandling_(true);
    }
  };
  
  
  /**
   * @param {!goog.events.BrowserEvent} e Event to handle.
   * @private
   */
  goog.ui.Container.prototype.preventPointerCapture_ = function(e) {
    var elem = /** @type {!Element} */ (e.target);
    if (!!elem.releasePointerCapture) {
      elem.releasePointerCapture(e.pointerId);
    }
  };
  
  
  /**
   * Sets up listening for events applicable to focusable containers.
   * @param {boolean} enable Whether to enable or disable focus handling.
   * @private
   */
  goog.ui.Container.prototype.enableFocusHandling_ = function(enable) {
    var handler = this.getHandler();
    var keyTarget = this.getKeyEventTarget();
    if (enable) {
      handler.listen(keyTarget, goog.events.EventType.FOCUS, this.handleFocus)
          .listen(keyTarget, goog.events.EventType.BLUR, this.handleBlur)
          .listen(
              this.getKeyHandler(), goog.events.KeyHandler.EventType.KEY,
              this.handleKeyEvent);
    } else {
      handler.unlisten(keyTarget, goog.events.EventType.FOCUS, this.handleFocus)
          .unlisten(keyTarget, goog.events.EventType.BLUR, this.handleBlur)
          .unlisten(
              this.getKeyHandler(), goog.events.KeyHandler.EventType.KEY,
              this.handleKeyEvent);
    }
  };
  
  
  /**
   * Cleans up the container before its DOM is removed from the document, and
   * removes event handlers.  Overrides {@link goog.ui.Component#exitDocument}.
   * @override
   */
  goog.ui.Container.prototype.exitDocument = function() {
    // {@link #setHighlightedIndex} has to be called before
    // {@link goog.ui.Component#exitDocument}, otherwise it has no effect.
    this.setHighlightedIndex(-1);
  
    if (this.openItem_) {
      this.openItem_.setOpen(false);
    }
  
    this.mouseButtonPressed_ = false;
  
    goog.ui.Container.superClass_.exitDocument.call(this);
  };
  
  
  /** @override */
  goog.ui.Container.prototype.disposeInternal = function() {
    goog.ui.Container.superClass_.disposeInternal.call(this);
  
    if (this.keyHandler_) {
      this.keyHandler_.dispose();
      this.keyHandler_ = null;
    }
  
    this.keyEventTarget_ = null;
    this.childElementIdMap_ = null;
    this.openItem_ = null;
    this.renderer_ = null;
  };
  
  
  // Default event handlers.
  
  
  /**
   * Handles ENTER events raised by child controls when they are navigated to.
   * @param {goog.events.Event} e ENTER event to handle.
   * @return {boolean} Whether to prevent handleMouseOver from handling
   *    the event.
   */
  goog.ui.Container.prototype.handleEnterItem = function(e) {
    // Allow the Control to highlight itself.
    return true;
  };
  
  
  /**
   * Handles HIGHLIGHT events dispatched by items in the container when
   * they are highlighted.
   * @param {goog.events.Event} e Highlight event to handle.
   */
  goog.ui.Container.prototype.handleHighlightItem = function(e) {
    var index = this.indexOfChild(/** @type {goog.ui.Control} */ (e.target));
    if (index > -1 && index != this.highlightedIndex_) {
      var item = this.getHighlighted();
      if (item) {
        // Un-highlight previously highlighted item.
        item.setHighlighted(false);
      }
  
      this.highlightedIndex_ = index;
      item = this.getHighlighted();
  
      if (this.isMouseButtonPressed()) {
        // Activate item when mouse button is pressed, to allow MacOS-style
        // dragging to choose menu items.  Although this should only truly
        // happen if the highlight is due to mouse movements, there is little
        // harm in doing it for keyboard or programmatic highlights.
        item.setActive(true);
      }
  
      // Update open item if open item needs follow highlight.
      if (this.openFollowsHighlight_ && this.openItem_ &&
          item != this.openItem_) {
        if (item.isSupportedState(goog.ui.Component.State.OPENED)) {
          item.setOpen(true);
        } else {
          this.openItem_.setOpen(false);
        }
      }
    }
  
    var element = this.getElement();
    goog.asserts.assert(
        element, 'The DOM element for the container cannot be null.');
    if (e.target.getElement() != null) {
      goog.a11y.aria.setState(
          element, goog.a11y.aria.State.ACTIVEDESCENDANT,
          e.target.getElement().id);
    }
  };
  
  
  /**
   * Handles UNHIGHLIGHT events dispatched by items in the container when
   * they are unhighlighted.
   * @param {goog.events.Event} e Unhighlight event to handle.
   */
  goog.ui.Container.prototype.handleUnHighlightItem = function(e) {
    if (e.target == this.getHighlighted()) {
      this.highlightedIndex_ = -1;
    }
    var element = this.getElement();
    goog.asserts.assert(
        element, 'The DOM element for the container cannot be null.');
    // Setting certain ARIA attributes to empty strings is problematic.
    // Just remove the attribute instead.
    goog.a11y.aria.removeState(element, goog.a11y.aria.State.ACTIVEDESCENDANT);
  };
  
  
  /**
   * Handles OPEN events dispatched by items in the container when they are
   * opened.
   * @param {goog.events.Event} e Open event to handle.
   */
  goog.ui.Container.prototype.handleOpenItem = function(e) {
    var item = /** @type {goog.ui.Control} */ (e.target);
    if (item && item != this.openItem_ && item.getParent() == this) {
      if (this.openItem_) {
        this.openItem_.setOpen(false);
      }
      this.openItem_ = item;
    }
  };
  
  
  /**
   * Handles CLOSE events dispatched by items in the container when they are
   * closed.
   * @param {goog.events.Event} e Close event to handle.
   */
  goog.ui.Container.prototype.handleCloseItem = function(e) {
    if (e.target == this.openItem_) {
      this.openItem_ = null;
    }
  
    var element = this.getElement();
    var targetEl = e.target.getElement();
    // Set the active descendant to the menu item when its submenu is closed and
    // it is still highlighted. This can sometimes be called when the menuitem is
    // unhighlighted because the focus moved elsewhere, do nothing at that point.
    if (element && e.target.isHighlighted() && targetEl) {
      goog.a11y.aria.setActiveDescendant(element, targetEl);
    }
  };
  
  
  /**
   * Handles mousedown events over the container.  The default implementation
   * sets the "mouse button pressed" flag and, if the container is focusable,
   * grabs keyboard focus.
   * @param {goog.events.BrowserEvent} e Mousedown event to handle.
   */
  goog.ui.Container.prototype.handleMouseDown = function(e) {
    if (this.enabled_) {
      this.setMouseButtonPressed(true);
    }
  
    var keyTarget = this.getKeyEventTarget();
    if (keyTarget && goog.dom.isFocusableTabIndex(keyTarget)) {
      // The container is configured to receive keyboard focus.
      keyTarget.focus();
    } else {
      // The control isn't configured to receive keyboard focus; prevent it
      // from stealing focus or destroying the selection.
      e.preventDefault();
    }
  };
  
  
  /**
   * Handles mouseup events over the document.  The default implementation
   * clears the "mouse button pressed" flag.
   * @param {goog.events.BrowserEvent} e Mouseup event to handle.
   */
  goog.ui.Container.prototype.handleDocumentMouseUp = function(e) {
    this.setMouseButtonPressed(false);
  };
  
  
  /**
   * Handles mouse events originating from nodes belonging to the controls hosted
   * in the container.  Locates the child control based on the DOM node that
   * dispatched the event, and forwards the event to the control for handling.
   * @param {goog.events.BrowserEvent} e Mouse event to handle.
   */
  goog.ui.Container.prototype.handleChildMouseEvents = function(e) {
    var MouseEventType = goog.ui.ComponentUtil.getMouseEventType(this);
  
    var control = this.getOwnerControl(/** @type {Node} */ (e.target));
    if (control) {
      // Child control identified; forward the event.
      switch (e.type) {
        case MouseEventType.MOUSEDOWN:
          control.handleMouseDown(e);
          break;
        case MouseEventType.MOUSEUP:
        case MouseEventType.MOUSECANCEL:
          control.handleMouseUp(e);
          break;
        case MouseEventType.MOUSEOVER:
          control.handleMouseOver(e);
          break;
        case MouseEventType.MOUSEOUT:
          control.handleMouseOut(e);
          break;
        case goog.events.EventType.CONTEXTMENU:
          control.handleContextMenu(e);
          break;
      }
    }
  };
  
  
  /**
   * Returns the child control that owns the given DOM node, or null if no such
   * control is found.
   * @param {Node} node DOM node whose owner is to be returned.
   * @return {goog.ui.Control?} Control hosted in the container to which the node
   *     belongs (if found).
   * @protected
   */
  goog.ui.Container.prototype.getOwnerControl = function(node) {
    // Ensure that this container actually has child controls before
    // looking up the owner.
    if (this.childElementIdMap_) {
      var elem = this.getElement();
      // See http://b/2964418 . IE9 appears to evaluate '!=' incorrectly, so
      // using '!==' instead.
      // TODO(user): Possibly revert this change if/when IE9 fixes the issue.
      while (node && node !== elem) {
        var id = node.id;
        if (id in this.childElementIdMap_) {
          return this.childElementIdMap_[id];
        }
        node = node.parentNode;
      }
    }
    return null;
  };
  
  
  /**
   * Handles focus events raised when the container's key event target receives
   * keyboard focus.
   * @param {goog.events.BrowserEvent} e Focus event to handle.
   */
  goog.ui.Container.prototype.handleFocus = function(e) {
    // No-op in the base class.
  };
  
  
  /**
   * Handles blur events raised when the container's key event target loses
   * keyboard focus.  The default implementation clears the highlight index.
   * @param {goog.events.BrowserEvent} e Blur event to handle.
   */
  goog.ui.Container.prototype.handleBlur = function(e) {
    this.setHighlightedIndex(-1);
    this.setMouseButtonPressed(false);
    // If the container loses focus, and one of its children is open, close it.
    if (this.openItem_) {
      this.openItem_.setOpen(false);
    }
  };
  
  
  /**
   * Attempts to handle a keyboard event, if the control is enabled, by calling
   * {@link handleKeyEventInternal}.  Considered protected; should only be used
   * within this package and by subclasses.
   * @param {goog.events.KeyEvent} e Key event to handle.
   * @return {boolean} Whether the key event was handled.
   */
  goog.ui.Container.prototype.handleKeyEvent = function(e) {
    if (this.isEnabled() && this.isVisible() &&
        (this.getChildCount() != 0 || this.keyEventTarget_) &&
        this.handleKeyEventInternal(e)) {
      e.preventDefault();
      e.stopPropagation();
      return true;
    }
    return false;
  };
  
  
  /**
   * Attempts to handle a keyboard event; returns true if the event was handled,
   * false otherwise.  If the container is enabled, and a child is highlighted,
   * calls the child control's `handleKeyEvent` method to give the control
   * a chance to handle the event first.
   * @param {goog.events.KeyEvent} e Key event to handle.
   * @return {boolean} Whether the event was handled by the container (or one of
   *     its children).
   */
  goog.ui.Container.prototype.handleKeyEventInternal = function(e) {
    // Give the highlighted control the chance to handle the key event.
    var highlighted = this.getHighlighted();
    if (highlighted && typeof highlighted.handleKeyEvent == 'function' &&
        highlighted.handleKeyEvent(e)) {
      return true;
    }
  
    // Give the open control the chance to handle the key event.
    if (this.openItem_ && this.openItem_ != highlighted &&
        typeof this.openItem_.handleKeyEvent == 'function' &&
        this.openItem_.handleKeyEvent(e)) {
      return true;
    }
  
    // Do not handle the key event if any modifier key is pressed.
    if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) {
      return false;
    }
  
    // Either nothing is highlighted, or the highlighted control didn't handle
    // the key event, so attempt to handle it here.
    switch (e.keyCode) {
      case goog.events.KeyCodes.ESC:
        if (this.isFocusable()) {
          this.getKeyEventTarget().blur();
        } else {
          return false;
        }
        break;
  
      case goog.events.KeyCodes.HOME:
        this.highlightFirst();
        break;
  
      case goog.events.KeyCodes.END:
        this.highlightLast();
        break;
  
      case goog.events.KeyCodes.UP:
        if (this.orientation_ == goog.ui.Container.Orientation.VERTICAL) {
          this.highlightPrevious();
        } else {
          return false;
        }
        break;
  
      case goog.events.KeyCodes.LEFT:
        if (this.orientation_ == goog.ui.Container.Orientation.HORIZONTAL) {
          if (this.isRightToLeft()) {
            this.highlightNext();
          } else {
            this.highlightPrevious();
          }
        } else {
          return false;
        }
        break;
  
      case goog.events.KeyCodes.DOWN:
        if (this.orientation_ == goog.ui.Container.Orientation.VERTICAL) {
          this.highlightNext();
        } else {
          return false;
        }
        break;
  
      case goog.events.KeyCodes.RIGHT:
        if (this.orientation_ == goog.ui.Container.Orientation.HORIZONTAL) {
          if (this.isRightToLeft()) {
            this.highlightPrevious();
          } else {
            this.highlightNext();
          }
        } else {
          return false;
        }
        break;
  
      default:
        return false;
    }
  
    return true;
  };
  
  
  // Child component management.
  
  
  /**
   * Creates a DOM ID for the child control and registers it to an internal
   * hash table to be able to find it fast by id.
   * @param {goog.ui.Component} child The child control. Its root element has
   *     to be created yet.
   * @private
   */
  goog.ui.Container.prototype.registerChildId_ = function(child) {
    // Map the DOM ID of the control's root element to the control itself.
    var childElem = child.getElement();
  
    // If the control's root element doesn't have a DOM ID assign one.
    var id = childElem.id || (childElem.id = child.getId());
  
    // Lazily create the child element ID map on first use.
    if (!this.childElementIdMap_) {
      this.childElementIdMap_ = {};
    }
    this.childElementIdMap_[id] = child;
  };
  
  
  /**
   * Adds the specified control as the last child of this container.  See
   * {@link goog.ui.Container#addChildAt} for detailed semantics.
   * @param {goog.ui.Component} child The new child control.
   * @param {boolean=} opt_render Whether the new child should be rendered
   *     immediately after being added (defaults to false).
   * @override
   */
  goog.ui.Container.prototype.addChild = function(child, opt_render) {
    goog.asserts.assertInstanceof(
        child, goog.ui.Control, 'The child of a container must be a control');
    goog.ui.Container.superClass_.addChild.call(this, child, opt_render);
  };
  
  
  /**
   * Overrides {@link goog.ui.Container#getChild} to make it clear that it
   * only returns {@link goog.ui.Control}s.
   * @param {string} id Child component ID.
   * @return {goog.ui.Control} The child with the given ID; null if none.
   * @override
   */
  goog.ui.Container.prototype.getChild;
  
  
  /**
   * Overrides {@link goog.ui.Container#getChildAt} to make it clear that it
   * only returns {@link goog.ui.Control}s.
   * @param {number} index 0-based index.
   * @return {goog.ui.Control} The child with the given ID; null if none.
   * @override
   */
  goog.ui.Container.prototype.getChildAt;
  
  
  /**
   * Adds the control as a child of this container at the given 0-based index.
   * Overrides {@link goog.ui.Component#addChildAt} by also updating the
   * container's highlight index.  Since {@link goog.ui.Component#addChild} uses
   * {@link #addChildAt} internally, we only need to override this method.
   * @param {goog.ui.Component} control New child.
   * @param {number} index Index at which the new child is to be added.
   * @param {boolean=} opt_render Whether the new child should be rendered
   *     immediately after being added (defaults to false).
   * @override
   */
  goog.ui.Container.prototype.addChildAt = function(control, index, opt_render) {
    goog.asserts.assertInstanceof(control, goog.ui.Control);
  
    // Make sure the child control dispatches HIGHLIGHT, UNHIGHLIGHT, OPEN, and
    // CLOSE events, and that it doesn't steal keyboard focus.
    control.setDispatchTransitionEvents(goog.ui.Component.State.HOVER, true);
    control.setDispatchTransitionEvents(goog.ui.Component.State.OPENED, true);
    if (this.isFocusable() || !this.isFocusableChildrenAllowed()) {
      control.setSupportedState(goog.ui.Component.State.FOCUSED, false);
    }
  
    // Disable mouse event handling by child controls.
    control.setHandleMouseEvents(false);
  
    var srcIndex =
        (control.getParent() == this) ? this.indexOfChild(control) : -1;
  
    // Let the superclass implementation do the work.
    goog.ui.Container.superClass_.addChildAt.call(
        this, control, index, opt_render);
  
    if (control.isInDocument() && this.isInDocument()) {
      this.registerChildId_(control);
    }
  
    this.updateHighlightedIndex_(srcIndex, index);
  };
  
  
  /**
   * Updates the highlighted index when children are added or moved.
   * @param {number} fromIndex Index of the child before it was moved, or -1 if
   *     the child was added.
   * @param {number} toIndex Index of the child after it was moved or added.
   * @private
   */
  goog.ui.Container.prototype.updateHighlightedIndex_ = function(
      fromIndex, toIndex) {
    if (fromIndex == -1) {
      fromIndex = this.getChildCount();
    }
    if (fromIndex == this.highlightedIndex_) {
      // The highlighted element itself was moved.
      this.highlightedIndex_ = Math.min(this.getChildCount() - 1, toIndex);
    } else if (
        fromIndex > this.highlightedIndex_ && toIndex <= this.highlightedIndex_) {
      // The control was added or moved behind the highlighted index.
      this.highlightedIndex_++;
    } else if (
        fromIndex < this.highlightedIndex_ && toIndex > this.highlightedIndex_) {
      // The control was moved from before to behind the highlighted index.
      this.highlightedIndex_--;
    }
  };
  
  
  /**
   * Removes a child control.  Overrides {@link goog.ui.Component#removeChild} by
   * updating the highlight index.  Since {@link goog.ui.Component#removeChildAt}
   * uses {@link #removeChild} internally, we only need to override this method.
   * @param {string|goog.ui.Component} control The ID of the child to remove, or
   *     the control itself.
   * @param {boolean=} opt_unrender Whether to call `exitDocument` on the
   *     removed control, and detach its DOM from the document (defaults to
   *     false).
   * @return {goog.ui.Control} The removed control, if any.
   * @override
   */
  goog.ui.Container.prototype.removeChild = function(control, opt_unrender) {
    control = goog.isString(control) ? this.getChild(control) : control;
    goog.asserts.assertInstanceof(control, goog.ui.Control);
  
    if (control) {
      var index = this.indexOfChild(control);
      if (index != -1) {
        if (index == this.highlightedIndex_) {
          control.setHighlighted(false);
          this.highlightedIndex_ = -1;
        } else if (index < this.highlightedIndex_) {
          this.highlightedIndex_--;
        }
      }
  
      // Remove the mapping from the child element ID map.
      var childElem = control.getElement();
      if (childElem && childElem.id && this.childElementIdMap_) {
        goog.object.remove(this.childElementIdMap_, childElem.id);
      }
    }
  
    control = /** @type {!goog.ui.Control} */ (
        goog.ui.Container.superClass_.removeChild.call(
            this, control, opt_unrender));
  
    // Re-enable mouse event handling (in case the control is reused elsewhere).
    control.setHandleMouseEvents(true);
  
    return control;
  };
  
  
  // Container state management.
  
  
  /**
   * Returns the container's orientation.
   * @return {?goog.ui.Container.Orientation} Container orientation.
   */
  goog.ui.Container.prototype.getOrientation = function() {
    return this.orientation_;
  };
  
  
  /**
   * Sets the container's orientation.
   * @param {goog.ui.Container.Orientation} orientation Container orientation.
   */
  // TODO(attila): Do we need to support containers with dynamic orientation?
  goog.ui.Container.prototype.setOrientation = function(orientation) {
    if (this.getElement()) {
      // Too late.
      throw new Error(goog.ui.Component.Error.ALREADY_RENDERED);
    }
  
    this.orientation_ = orientation;
  };
  
  
  /**
   * Returns true if the container's visibility is set to visible, false if
   * it is set to hidden.  A container that is set to hidden is guaranteed
   * to be hidden from the user, but the reverse isn't necessarily true.
   * A container may be set to visible but can otherwise be obscured by another
   * element, rendered off-screen, or hidden using direct CSS manipulation.
   * @return {boolean} Whether the container is set to be visible.
   */
  goog.ui.Container.prototype.isVisible = function() {
    return this.visible_;
  };
  
  
  /**
   * Shows or hides the container.  Does nothing if the container already has
   * the requested visibility.  Otherwise, dispatches a SHOW or HIDE event as
   * appropriate, giving listeners a chance to prevent the visibility change.
   * @param {boolean} visible Whether to show or hide the container.
   * @param {boolean=} opt_force If true, doesn't check whether the container
   *     already has the requested visibility, and doesn't dispatch any events.
   * @return {boolean} Whether the visibility was changed.
   */
  goog.ui.Container.prototype.setVisible = function(visible, opt_force) {
    if (opt_force || (this.visible_ != visible &&
                      this.dispatchEvent(
                          visible ? goog.ui.Component.EventType.SHOW :
                                    goog.ui.Component.EventType.HIDE))) {
      this.visible_ = visible;
  
      var elem = this.getElement();
      if (elem) {
        goog.style.setElementShown(elem, visible);
        if (this.isFocusable()) {
          // Enable keyboard access only for enabled & visible containers.
          this.renderer_.enableTabIndex(
              this.getKeyEventTarget(), this.enabled_ && this.visible_);
        }
        if (!opt_force) {
          this.dispatchEvent(
              this.visible_ ? goog.ui.Container.EventType.AFTER_SHOW :
                              goog.ui.Container.EventType.AFTER_HIDE);
        }
      }
  
      return true;
    }
  
    return false;
  };
  
  
  /**
   * Returns true if the container is enabled, false otherwise.
   * @return {boolean} Whether the container is enabled.
   */
  goog.ui.Container.prototype.isEnabled = function() {
    return this.enabled_;
  };
  
  
  /**
   * Enables/disables the container based on the `enable` argument.
   * Dispatches an `ENABLED` or `DISABLED` event prior to changing
   * the container's state, which may be caught and canceled to prevent the
   * container from changing state.  Also enables/disables child controls.
   * @param {boolean} enable Whether to enable or disable the container.
   */
  goog.ui.Container.prototype.setEnabled = function(enable) {
    if (this.enabled_ != enable &&
        this.dispatchEvent(
            enable ? goog.ui.Component.EventType.ENABLE :
                     goog.ui.Component.EventType.DISABLE)) {
      if (enable) {
        // Flag the container as enabled first, then update children.  This is
        // because controls can't be enabled if their parent is disabled.
        this.enabled_ = true;
        this.forEachChild(function(child) {
          // Enable child control unless it is flagged.
          if (child.wasDisabled) {
            delete child.wasDisabled;
          } else {
            child.setEnabled(true);
          }
        });
      } else {
        // Disable children first, then flag the container as disabled.  This is
        // because controls can't be disabled if their parent is already disabled.
        this.forEachChild(function(child) {
          // Disable child control, or flag it if it's already disabled.
          if (child.isEnabled()) {
            child.setEnabled(false);
          } else {
            child.wasDisabled = true;
          }
        });
        this.enabled_ = false;
        this.setMouseButtonPressed(false);
      }
  
      if (this.isFocusable()) {
        // Enable keyboard access only for enabled & visible components.
        this.renderer_.enableTabIndex(
            this.getKeyEventTarget(), enable && this.visible_);
      }
    }
  };
  
  
  /**
   * Returns true if the container is focusable, false otherwise.  The default
   * is true.  Focusable containers always have a tab index and allocate a key
   * handler to handle keyboard events while focused.
   * @return {boolean} Whether the component is focusable.
   */
  goog.ui.Container.prototype.isFocusable = function() {
    return this.focusable_;
  };
  
  
  /**
   * Sets whether the container is focusable.  The default is true.  Focusable
   * containers always have a tab index and allocate a key handler to handle
   * keyboard events while focused.
   * @param {boolean} focusable Whether the component is to be focusable.
   */
  goog.ui.Container.prototype.setFocusable = function(focusable) {
    if (focusable != this.focusable_ && this.isInDocument()) {
      this.enableFocusHandling_(focusable);
    }
    this.focusable_ = focusable;
    if (this.enabled_ && this.visible_) {
      this.renderer_.enableTabIndex(this.getKeyEventTarget(), focusable);
    }
  };
  
  
  /**
   * Returns true if the container allows children to be focusable, false
   * otherwise.  Only effective if the container is not focusable.
   * @return {boolean} Whether children should be focusable.
   */
  goog.ui.Container.prototype.isFocusableChildrenAllowed = function() {
    return this.allowFocusableChildren_;
  };
  
  
  /**
   * Sets whether the container allows children to be focusable, false
   * otherwise.  Only effective if the container is not focusable.
   * @param {boolean} focusable Whether the children should be focusable.
   */
  goog.ui.Container.prototype.setFocusableChildrenAllowed = function(focusable) {
    this.allowFocusableChildren_ = focusable;
  };
  
  
  /**
   * @return {boolean} Whether highlighting a child component should also open it.
   */
  goog.ui.Container.prototype.isOpenFollowsHighlight = function() {
    return this.openFollowsHighlight_;
  };
  
  
  /**
   * Sets whether highlighting a child component should also open it.
   * @param {boolean} follow Whether highlighting a child component also opens it.
   */
  goog.ui.Container.prototype.setOpenFollowsHighlight = function(follow) {
    this.openFollowsHighlight_ = follow;
  };
  
  
  // Highlight management.
  
  
  /**
   * Returns the index of the currently highlighted item (-1 if none).
   * @return {number} Index of the currently highlighted item.
   */
  goog.ui.Container.prototype.getHighlightedIndex = function() {
    return this.highlightedIndex_;
  };
  
  
  /**
   * Highlights the item at the given 0-based index (if any).  If another item
   * was previously highlighted, it is un-highlighted.
   * @param {number} index Index of item to highlight (-1 removes the current
   *     highlight).
   */
  goog.ui.Container.prototype.setHighlightedIndex = function(index) {
    var child = this.getChildAt(index);
    if (child) {
      child.setHighlighted(true);
    } else if (this.highlightedIndex_ > -1) {
      this.getHighlighted().setHighlighted(false);
    }
  };
  
  
  /**
   * Highlights the given item if it exists and is a child of the container;
   * otherwise un-highlights the currently highlighted item.
   * @param {goog.ui.Control} item Item to highlight.
   */
  goog.ui.Container.prototype.setHighlighted = function(item) {
    this.setHighlightedIndex(this.indexOfChild(item));
  };
  
  
  /**
   * Returns the currently highlighted item (if any).
   * @return {goog.ui.Control?} Highlighted item (null if none).
   */
  goog.ui.Container.prototype.getHighlighted = function() {
    return this.getChildAt(this.highlightedIndex_);
  };
  
  
  /**
   * Highlights the first highlightable item in the container
   */
  goog.ui.Container.prototype.highlightFirst = function() {
    this.highlightHelper(function(index, max) {
      return (index + 1) % max;
    }, this.getChildCount() - 1);
  };
  
  
  /**
   * Highlights the last highlightable item in the container.
   */
  goog.ui.Container.prototype.highlightLast = function() {
    this.highlightHelper(function(index, max) {
      index--;
      return index < 0 ? max - 1 : index;
    }, 0);
  };
  
  
  /**
   * Highlights the next highlightable item (or the first if nothing is currently
   * highlighted).
   */
  goog.ui.Container.prototype.highlightNext = function() {
    this.highlightHelper(function(index, max) {
      return (index + 1) % max;
    }, this.highlightedIndex_);
  };
  
  
  /**
   * Highlights the previous highlightable item (or the last if nothing is
   * currently highlighted).
   */
  goog.ui.Container.prototype.highlightPrevious = function() {
    this.highlightHelper(function(index, max) {
      index--;
      return index < 0 ? max - 1 : index;
    }, this.highlightedIndex_);
  };
  
  
  /**
   * Helper function that manages the details of moving the highlight among
   * child controls in response to keyboard events.
   * @param {function(this: goog.ui.Container, number, number) : number} fn
   *     Function that accepts the current and maximum indices, and returns the
   *     next index to check.
   * @param {number} startIndex Start index.
   * @return {boolean} Whether the highlight has changed.
   * @protected
   */
  goog.ui.Container.prototype.highlightHelper = function(fn, startIndex) {
    // If the start index is -1 (meaning there's nothing currently highlighted),
    // try starting from the currently open item, if any.
    var curIndex =
        startIndex < 0 ? this.indexOfChild(this.openItem_) : startIndex;
    var numItems = this.getChildCount();
  
    curIndex = fn.call(this, curIndex, numItems);
    var visited = 0;
    while (visited <= numItems) {
      var control = this.getChildAt(curIndex);
      if (control && this.canHighlightItem(control)) {
        this.setHighlightedIndexFromKeyEvent(curIndex);
        return true;
      }
      visited++;
      curIndex = fn.call(this, curIndex, numItems);
    }
    return false;
  };
  
  
  /**
   * Returns whether the given item can be highlighted.
   * @param {goog.ui.Control} item The item to check.
   * @return {boolean} Whether the item can be highlighted.
   * @protected
   */
  goog.ui.Container.prototype.canHighlightItem = function(item) {
    return item.isVisible() && item.isEnabled() &&
        item.isSupportedState(goog.ui.Component.State.HOVER);
  };
  
  
  /**
   * Helper method that sets the highlighted index to the given index in response
   * to a keyboard event.  The base class implementation simply calls the
   * {@link #setHighlightedIndex} method, but subclasses can override this
   * behavior as needed.
   * @param {number} index Index of item to highlight.
   * @protected
   */
  goog.ui.Container.prototype.setHighlightedIndexFromKeyEvent = function(index) {
    this.setHighlightedIndex(index);
  };
  
  
  /**
   * Returns the currently open (expanded) control in the container (null if
   * none).
   * @return {goog.ui.Control?} The currently open control.
   */
  goog.ui.Container.prototype.getOpenItem = function() {
    return this.openItem_;
  };
  
  
  /**
   * Returns true if the mouse button is pressed, false otherwise.
   * @return {boolean} Whether the mouse button is pressed.
   */
  goog.ui.Container.prototype.isMouseButtonPressed = function() {
    return this.mouseButtonPressed_;
  };
  
  
  /**
   * Sets or clears the "mouse button pressed" flag.
   * @param {boolean} pressed Whether the mouse button is presed.
   */
  goog.ui.Container.prototype.setMouseButtonPressed = function(pressed) {
    this.mouseButtonPressed_ = pressed;
  };
  