Source: src/main/javascript/oracle/oj/ojcollapsible/ojcollapsible.js

Oracle® JavaScript Extension Toolkit (JET)
1.1.2

E65298-01

/**
 * Copyright (c) 2014, Oracle and/or its affiliates.
 * All rights reserved.
 */

/**
 * @preserve Copyright 2013 jQuery Foundation and other contributors
 * Released under the MIT license.
 * http://jquery.org/license
 */

/**
 * @ojcomponent oj.ojCollapsible
 * @augments oj.baseComponent
 * @since 0.6
 * 
 * @classdesc
 * <h3 id="collapsibleOverview-section">
 *   JET Collapsible Component
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#collapsibleOverview-section"></a>
 * </h3>
 * 
 * <p>Description: Themeable, WAI-ARIA-compliant collapsible with mouse and keyboard interactions for navigation.
 * 
 * <p>A JET Collapsible can be created from any valid markup as long as the root element has at least two children: the first element for the header and the second element for the content.
 * 
 * <pre class="prettyprint">
 * <code>
 * <div id="collapsible">
 *   <h3>Header 1</h3>
 *   <p>Content 1</p>
 * </div>
 * </code></pre>
 * 
 * <h3 id="touch-section">
 *   Touch End User Information
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#touch-section"></a>
 * </h3>
 *
 * {@ojinclude "name":"touchDoc"}
 *
 *
 * <h3 id="keyboard-section">
 *   Keyboard End User Information
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#keyboard-section"></a>
 * </h3>
 * 
 * {@ojinclude "name":"keyboardDoc"}
 *
 * 
 * <h3 id="rtl-section">
 *   Reading direction
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#rtl-section"></a>
 * </h3>
 * 
 * <p>As with any JET component, in the unusual case that the directionality (LTR or RTL) changes post-init, the collapsible must be <code class="prettyprint">refresh()</code>ed.
 * 
 * 
 * <h3 id="pseudos-section">
 *   Pseudo-selectors
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#pseudos-section"></a>
 * </h3>
 * 
 * <p>The <code class="prettyprint">:oj-collapsible</code> pseudo-selector can be used in jQuery expressions to select JET Collapsible.  For example:
 * 
 * <pre class="prettyprint">
 * <code>$( ":oj-collapsible" ) // selects all JET Collapsible on the page
 * $myEventTarget.closest( ":oj-collapsible" ) // selects the closest ancestor that is a JET Collapsible
 * </code></pre>
 * 
 * 
 * <h3 id="jqui2jet-section">
 *   JET for jQuery UI developers
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#jqui2jet-section"></a>
 * </h3>
 * 
 * <ol>
 *   <li>JET Collapsible supports expandArea option: specifies where to click to toggle disclosure. Default is "disclosureIcon", however if a collapsible is inside an Accordion, default is "header"</li>
 * </ol>
 * 
 * <p>Also, event names for all JET components are prefixed with "oj", instead of component-specific prefixes like "collapsible".  
 * 
 * <!-- - - - - Above this point, the tags are for the class.
 *              Below this point, the tags are for the constructor (initializer). - - - - - - -->
 * 
 * @desc Creates a JET Collapsible. 
 * @example <caption>Initialize the collapsible with no options specified:</caption>
 * $( ".selector" ).ojCollapsible();
 * 
 * @example <caption>Initialize the collapsible with some options specified:</caption>
 * $( ".selector" ).ojCollapsible( { "expanded": true } );
 * 
 * @example <caption>Initialize the collapsible via the JET <code class="prettyprint">ojComponent</code> binding:</caption>
 * <div id="collapsible" data-bind="ojComponent: { component: 'ojCollapsible', expanded: true}">
 *
 */
(function ()
{
  var uid = 0, 
      OPEN_ICON = "oj-collapsible-open-icon", 
      CLOSE_ICON = "oj-collapsible-close-icon";

  oj.__registerWidget("oj.ojCollapsible", $['oj']['baseComponent'], 
  {
    widgetEventPrefix : "oj",
    options : 
    {
      /** 
       * Specifies if the content is expanded.
       *
       * @expose 
       * @memberof! oj.ojCollapsible
       * @instance
       * @type {boolean}
       * @default <code class="prettyprint">false</code>
       *
       * @example <caption>Initialize the collapsible with the <code class="prettyprint">expanded</code> option specified:</caption>
       * $( ".selector" ).ojCollapsible( { "expanded": true } );
       * 
       * @example <caption>Get or set the <code class="prettyprint">expanded</code> option, after initialization:</caption>
       * // getter
       * var expanded = $( ".selector" ).ojCollapsible( "option", "expanded" );
       * 
       * // setter
       * $( ".selector" ).ojCollapsible( "option", "expanded", true );
       */
      expanded : false, 

      /** 
       * Disables the collapsible if set to <code class="prettyprint">true</code>.
       * @name disabled
       * @memberof! oj.ojCollapsible
       * @instance
       * @type {boolean|null}
       * @default <code class="prettyprint">false</code>
       * @example <caption>Initialize the collapsible with the <code class="prettyprint">disabled</code> option specified:</caption>
       * $( ".selector" ).ojCollapsible( { "disabled": true } );
       * 
       * @example <caption>Get or set the <code class="prettyprint">disabled</code> option, after initialization:</caption>
       * // getter
       * var disabled = $( ".selector" ).ojCollapsible( "option", "disabled" );
       * 
       * // setter
       * $( ".selector" ).ojCollapsible( "option", "disabled", true );
       */
      disabled: null,

      /** 
       * The type of event to expand/collapse the collapsible. 
       * To expand the collapsible on hover, use "mouseover".
       *
       * @expose 
       * @memberof! oj.ojCollapsible
       * @instance
       * @type {string}
       * @default <code class="prettyprint">"click"</code>
       *
       * @example <caption>Initialize the collapsible with the <code class="prettyprint">expandOn</code> option specified:</caption>
       * $( ".selector" ).ojCollapsible( { "expandOn": "mouseover" } );
       * 
       * @example <caption>Get or set the <code class="prettyprint">expandOn</code> option, after initialization:</caption>
       * // getter
       * var expandOn = $( ".selector" ).ojCollapsible( "option", "expandOn" );
       * 
       * // setter
       * $( ".selector" ).ojCollapsible( "option", "expandOn", "mouseover" );
       */
      expandOn : "click", 

      /** 
       * Where in the header to click to toggle disclosure. Valid values: disclosureIcon or header
       *
       * @expose 
       * @memberof! oj.ojCollapsible
       * @instance
       * @type {string}
       * @default <code class="prettyprint">"disclosureIcon"</code>
       *
       * @example <caption>Initialize the collapsible with the <code class="prettyprint">expandArea</code> option specified:</caption>
       * $( ".selector" ).ojCollapsible( { "expandArea": "header" } );
       * 
       * @example <caption>Get or set the <code class="prettyprint">expandArea</code> option, after initialization:</caption>
       * // getter
       * var expandArea = $( ".selector" ).ojCollapsible( "option", "expandArea" );
       * 
       * // setter
       * $( ".selector" ).ojCollapsible( "option", "expandArea", "header" );
       */
      expandArea : "disclosureIcon",

      // callbacks
      /**
       * Triggered immediately before the collapsible is expanded.
       * beforeExpand can be canceled to prevent the content from expanding by returning a false in the event listener.
       *
       * @expose 
       * @event 
       * @memberof! oj.ojCollapsible
       * @instance
       * @property {Event} event <code class="prettyprint">jQuery</code> event object
       * @property {Object} ui Parameters
       * @property {jQuery} ui.header The header that is about to be expanded.
       * @property {jQuery} ui.content The content that is about to be expanded.
       * 
       * @example <caption>Initialize the collapsible with the <code class="prettyprint">beforeExpand</code> callback specified:</caption>
       * $( ".selector" ).ojCollapsible({
       *     "beforeExpand": function( event, ui ) {}
       * });
       *
       * @example <caption>Bind an event listener to the <code class="prettyprint">ojbeforeexpand</code> event:</caption>
       * $( ".selector" ).on( "ojbeforeexpand", function( event, ui ) {} );
       */
      beforeExpand : null,

      /**
       * Triggered after the collapsible has been expanded (after animation completes).
       *
       * @expose 
       * @event 
       * @memberof oj.ojCollapsible
       * @instance
       * @property {Event} event <code class="prettyprint">jQuery</code> event object
       * @property {Object} ui Parameters
       * @property {jQuery} ui.header The header that was just expanded.
       * @property {jQuery} ui.content The content that was just expanded.
       * 
       * @example <caption>Initialize the collapsible with the <code class="prettyprint">expand</code> callback specified:</caption>
       * $( ".selector" ).ojCollapsible({
       *     "expand": function( event, ui ) {}
       * });
       *
       * @example <caption>Bind an event listener to the <code class="prettyprint">ojexpand</code> event:</caption>
       * $( ".selector" ).on( "ojexpand", function( event, ui ) {} );
       */
      expand : null, 

      /**
       * Triggered immediately before the collapsible is collapsed.
       * beforeCollapse can be canceled to prevent the content from collapsing by returning a false in the event listener.
       *
       * @expose 
       * @event 
       * @memberof! oj.ojCollapsible
       * @instance
       * @property {Event} event <code class="prettyprint">jQuery</code> event object
       * @property {Object} ui Parameters
       * @property {jQuery} ui.header The header that is about to be collapsed.
       * @property {jQuery} ui.content The content that is about to be collapsed.
       * 
       * @example <caption>Initialize the collapsible with the <code class="prettyprint">beforeCollapse</code> callback specified:</caption>
       * $( ".selector" ).ojCollapsible({
       *     "beforeCollapse": function( event, ui ) {}
       * });
       *
       * @example <caption>Bind an event listener to the <code class="prettyprint">ojbeforecollapse</code> event:</caption>
       * $( ".selector" ).on( "ojbeforecollapse", function( event, ui ) {} );
       */
      beforeCollapse : null,

      /**
       * Triggered after the collapsible has been collapsed.
       *
       * @expose 
       * @event 
       * @memberof oj.ojCollapsible
       * @instance
       * @property {Event} event <code class="prettyprint">jQuery</code> event object
       * @property {Object} ui Parameters
       * @property {jQuery} ui.header The header that was just collapsed.
       * @property {jQuery} ui.content The content that was just collapsed.
       * 
       * @example <caption>Initialize the collapsible with the <code class="prettyprint">collapse</code> callback specified:</caption>
       * $( ".selector" ).ojCollapsible({
       *     "collapse": function( event, ui ) {}
       * });
       *
       * @example <caption>Bind an event listener to the <code class="prettyprint">ojcollapse</code> event:</caption>
       * $( ".selector" ).on( "ojcollapse", function( event, ui ) {} );
       */
      collapse : null, 

      /**
       * Fired whenever a supported component option changes, whether due to user interaction or programmatic
       * intervention.  If the new value is the same as the previous value, no event will be fired.
       *
       * Currently there is one supported option, <code class="prettyprint">"expanded"</code>.  Additional
       * options may be supported in the future, so listeners should verify which option is changing
       * before taking any action.
       *
       * @expose
       * @event
       * @memberof! oj.ojCollapsible
       * @instance
       * @property {Event} event <code class="prettyprint">jQuery</code> event object
       * @property {Object} ui Parameters
       * @property {string} ui.option the name of the option that is changing
       * @property {boolean} ui.previousValue the previous value of the option
       * @property {boolean} ui.value the current value of the option
       * @property {Object} ui.optionMetadata information about the option that is changing
       * @property {string} ui.optionMetadata.writeback <code class="prettyprint">"shouldWrite"</code> or
       *           <code class="prettyprint">"shouldNotWrite"</code>.  For use by the JET writeback mechanism.
       *
       */
      optionChange: null

    },

    _ComponentCreate : function ()
    {
      this._super();

      //Bug 18633679 - Stop using ui-helper-reset in the layout widgets.
      this.element.addClass("oj-collapsible oj-component")

      this._processPanels();
      this._refresh();

      //Bug 20028454 - collapsible shouldn't implement _init()
      this._initialRender = true;

      //don't fire event on initial render
      var elem = this.element[0];
      this._expandCollapseHandler(
      {
        type : this.options.expanded ? "ojexpand" : "ojcollapse",
        target : elem,
        currentTarget : elem,
        preventDefault : $.noop
      });

      this._initialRender = undefined;

    },

    // Override to set custom launcher
    _NotifyContextMenuGesture: function(menu, event, eventType)
    {
      // Setting the launcher to the "twisty" icon, since that seems to be the only tabbable thing in the collapsible, 
      // and it seems to remain tabbable even if the collapsible is disabled.  See the superclass JSDoc for _OpenContextMenu 
      // for tips on choosing a launcher.
      this._OpenContextMenu(event, eventType, {"launcher": this.element.find(".oj-collapsible-header-icon").first()});
    },

    _createIcons : function ()
    {
      var options = this.options;
      var icon = (options.expanded ? OPEN_ICON : CLOSE_ICON);
      var iconTag = this._isDisabled() ? $("<span>") : $("<a href='#'>");

      iconTag.addClass("oj-component-icon oj-clickable-icon-nocontext oj-collapsible-header-icon " + icon)
        .attr("aria-labelledby", this.header.attr( "id" ))
        .prependTo(this.header); //@HTMLUpdateOK

    },

    _destroyIcons : function ()
    {
      this.header
        .children(".oj-collapsible-header-icon")
        .remove();
    },

    _destroy : function ()
    {
      this._cleanup();

      // clean up main element
      this.element
        .removeClass("oj-collapsible oj-component oj-expanded oj-collapsed oj-disabled");

      // clean up headers
      if (this._isDisabled())
        this._findFocusables(this.header).removeAttr("tabIndex");

      this.header
        .removeClass("oj-collapsible-header")
        .each(function ()
        {
          if (/^oj-collapsible/.test(this.id))
          {
            this.removeAttribute("id");
          }
        });

      //aria
      var focusable = this._findFirstFocusableInHeader();
      focusable.removeAttr("role")
        .removeAttr("aria-controls")
        .removeAttr("aria-expanded")
        .removeAttr("aria-disabled");

      this._destroyIcons();

      // clean up content panels
      this.content
        .css("display", "")
        .removeAttr("aria-hidden")
        .removeAttr("tabIndex")
        .removeClass("oj-component-content oj-collapsible-content")
        .each(function ()
        {
          if (/^oj-collapsible/.test(this.id))
          {
            this.removeAttribute("id");
          }
        });
    },

    _cleanup : function ()
    {
      //remove listeners
      this._tearDownEvents();

      //remove wrapper
      if (this.content)
      {
        // Bug #19639907 - DomUtils.unwrap() will avoid unwrapping if the node is being destroyed by Knockout
        oj.DomUtils.unwrap(this.content);
        this.wrapper = null;
      }
      //TODO: remove oj-disabled

    },

    _isDisabled : function ()
    {
      return this.element.hasClass("oj-disabled");
    },

    _getExpandAreaSelector : function ()
    {
      if (this.options.expandArea == "header")
        return "> .oj-collapsible-header";
      else //disclosureIcon
        return "> .oj-collapsible-header > .oj-collapsible-header-icon";
    },

    _setOption : function (key, value, flags)
    {
      if (key === "expanded")
      {
        if (value === this.options.expanded)
          return;
        this._setExpanded(value);
        return;
      }

      // #5332 - opacity doesn't cascade to positioned elements in IE
      // so we need to add the disabled class to the headers and panels
//TODO: ignore disabled until disabled propagation is supported
      if (key === "disabled")
      {
        this.header
          .add(this.header.next());
        this.element.toggleClass("oj-disabled", !!value);

        //Bug 19707333 - a collapsible contained within a disabled collapsible appears disabled
        return;
      }

      if (key === "expandOn" || key === "expandArea")
      {
        this._tearDownEvents();
        this._super(key, value, flags);
        this._setupEvents();
      }
      else 
      {
        this._super(key, value, flags);
      }

    },

    _keydown : function (event)
    {
      if (event.altKey || event.ctrlKey)
      {
        return;
      }
      var keyCode = $.ui.keyCode;

      switch (event.keyCode)
      {
        case keyCode.SPACE:
        case keyCode.ENTER:
          this._toggleHandler(event);
          break;
      }
    },

    /**
     * Refreshes the visual state of the collapsible. JET components require a <code class="prettyprint">refresh()</code> or re-init after the DOM is 
     * programmatically changed underneath the component.
     * 
     * <p>This method does not accept any arguments.
     * 
     * @expose 
     * @memberof! oj.ojCollapsible
     * @instance
     * 
     * @example <caption>Invoke the <code class="prettyprint">refresh</code> method:</caption>
     * $( ".selector" ).ojCollapsible( "refresh" );
     */
    refresh : function ()
    {
      this._super();
      this._cleanup();
      this._processPanels();
      this._destroyIcons();
      this._refresh();
    },

    _processPanels : function ()
    {
      //Bug 18633679 - Stop using ui-helper-reset in the layout widgets.
      this.header = this.element.children(":first-child")
                      .addClass("oj-collapsible-header");

      this.content = this.header.next()
        .addClass("oj-collapsible-content oj-component-content");

      this.content.wrap("<div></div>"); //@HTMLUpdateOK
      this.wrapper = this.content.parent()
        .addClass("oj-collapsible-wrapper");

      if (this.options.disabled)
        this.element.addClass("oj-disabled");

      //Note: must set tabIndex=-1 to focusable elements
      //to avoid tabbing in a disabled header
      if (this._isDisabled())
      {
        this._findFocusables(this.header)
          .attr("tabIndex",  -1);
      }
    },

    /**
     * Used for explicit cases where the component needs to be refreshed 
     * (e.g., when the value option changes or other UI gestures).
     * @private
     */
    _refresh : function ()
    {
      var header = this.header, 
          content = this.content, 
          options = this.options;

      var collapsibleId = this.collapsibleId = 
        "oj-collapsible-" + (this.element.attr("id") || ++uid);

      var headerId = header.attr("id"), 
          contentId = content.attr("id");

      if (!headerId)
      {
        headerId = collapsibleId + "-header";
        header.attr("id", headerId);
      }
      if (!contentId)
      {
        contentId = collapsibleId + "-content";
        content.attr("id", contentId);
      }

      //aria
      this._createIcons();
      var focusable = this._findFirstFocusableInHeader();
      focusable.attr("role", "button")
        .attr("aria-controls", contentId)
        .attr("aria-expanded", options.expanded);

      if (this._isDisabled())
      {
        focusable.attr("aria-disabled", "true");
      }

      //Bug 19277743 - when collapsible is refreshed, it's content displays & disclosure icon collapsed
      if (options.expanded)
      {
        content.removeAttr("aria-hidden");
      }
      else 
      {
        this.wrapper.css({
          'max-height': 0,
          'overflow-y': 'hidden',
          'display': 'none'
        });
        this.wrapper.css('max-height', 0);
        content.attr("aria-hidden", "true");
      }

      this._setupEvents();
    },

    _setExpanded : function (expanded)
    {
      if (expanded)
        this.expand(true);
      else 
        this.collapse(true);
    },

    _setupEvents : function ()
    {
      var events = 
      {
        "keydown" : this._keydown
      };

      var event = this.options.expandOn;
      if (event)
      {
        var self = this ;
        $.each(event.split(" "), function (index, eventName)
        {
          //security test
          if (oj.DomUtils.isValidIdentifier(eventName))
          {
            events[eventName] = self._toggleHandler;
          }
        });
      }

      var expandArea = this.element.find(this._getExpandAreaSelector());

      //add listeners on expandArea (event expandArea)
      this._on(expandArea, events);

      this._on(this.wrapper, 
      {
        "transitionend" : this._transitionEndHandler, 
        "webkitTransitionEnd" : this._transitionEndHandler, 
        "otransitionend" : this._transitionEndHandler, 
        "oTransitionEnd" : this._transitionEndHandler
      });

      if (!this._isDisabled())
      {
        this._on(this.element, 
        {
          "ojexpand" : this._expandCollapseHandler, 
          "ojcollapse" : this._expandCollapseHandler, 
          "ojfocus" : this._focusHandler,
          "ojfocusout" : this._focusHandler
        });

        this._hoverable(expandArea);
        this._focusable(expandArea);
        this._activeable(expandArea);

      }
    },

    _tearDownEvents : function ()
    {
      this._off(this.element.find(this._getExpandAreaSelector()));

      //remove wrapper listeners
      if (this.wrapper)
        this._off(this.wrapper);
      this._off(this.element.add(this.content));
    },

    _toggleHandler : function (event)
    {
      if (this._isDisabled())
        return;

      if (this.options.expanded)
        this.collapse(true, event);
      else 
        this.expand(true, event);

      event.preventDefault();
      event.stopPropagation();

      //set focus on the disclosure icon
      this.header.find(".oj-collapsible-header-icon").focus();

    },

    _expandCollapseHandler : function (event)
    {
      if (this._isDisabled())
        return;

      if (event.target !== this.element[0])
        return;

      if (this._initialRender || !event.isDefaultPrevented())
      {
        var element = this.element, 
            options = this.options, 
            content = this.content,
            wrapper = this.wrapper,
            isExpanded = (event.type === "ojexpand");

        event.preventDefault();

        //Bug 19004479 - expansion animation on initial render.
        //skip animation
        if (this._initialRender) {

          //Bug 20028454 - collapsible shouldn't implement _init()
          options.expanded = isExpanded;
          if (isExpanded)
          {
            element.removeClass("oj-collapsed");
            element.addClass("oj-expanded");

            //Bug 19720502 - ojcollapsible needs to call oj.components.subtreeshown()/subtreehidden()
            oj.Components.subtreeShown(wrapper[0]);
          }
          else
          {
            element.removeClass("oj-expanded");
            element.addClass("oj-collapsed");
            wrapper.css('max-height', 0);
            wrapper.hide();

            //Bug 19720502 - ojcollapsible needs to call oj.components.subtreeshown()/subtreehidden()
            oj.Components.subtreeHidden(wrapper[0]);
          }
        }
        // do animation
        else 
        {
          //fire option change event
          this._changeExpandedOption(isExpanded, event);

          wrapper.contentHeight = wrapper.outerHeight();

          //expanding
          if (isExpanded)
          {
            //James: set display:none on the wrapper when it is hidden and then remove display:none when its is shown. 
            //This should trigger JAWS into refreshing the buffer.
            wrapper.show();

            setTimeout(function()
            {
              wrapper.contentHeight += content.outerHeight(); // if closed, add inner height to content height
              wrapper.addClass('oj-collapsible-transition').css(
              {
                'max-height': wrapper.contentHeight
              });
              element.removeClass("oj-collapsed");
              element.addClass("oj-expanded");

              //Bug 19720502 - ojcollapsible needs to call oj.components.subtreeshown()/subtreehidden()
              oj.Components.subtreeShown(wrapper[0]);

            }, 1);
          }

          //collapsing
          else
          {
            // disable transitions & set max-height to content height
            wrapper.removeClass("oj-collapsible-transition");
            wrapper.css({
              'max-height': wrapper.contentHeight,
              'overflow-y': 'hidden'
            });

            setTimeout(function()
            {
              // enable & start transition
              wrapper.addClass('oj-collapsible-transition')
                .css({
                  'max-height': 0   //!important
                });

              element.removeClass("oj-expanded");
              element.addClass("oj-collapsed");

              //Bug 19720502 - ojcollapsible needs to call oj.components.subtreeshown()/subtreehidden()
              oj.Components.subtreeHidden(wrapper[0]);

            }, 10); // 10ms timeout is the secret ingredient for disabling/enabling transitions
            // chrome only needs 1ms but FF needs ~10ms or it chokes on the first animation for some reason
          }
        }

        this.header
          .find(".oj-collapsible-header-icon").toggleClass(OPEN_ICON, isExpanded)
            // logic or cause same icon for expanded/collapsed state would remove the oj-icon-class
            .toggleClass(CLOSE_ICON, (! isExpanded || OPEN_ICON === CLOSE_ICON))
          .end();

        //aria
        if (isExpanded)
          this.content.removeAttr("aria-hidden")
        else
          this.content.attr("aria-hidden", "true");

        this._findFirstFocusableInHeader().attr("aria-expanded", isExpanded);

      }
    },

    _focusHandler : function (event)
    {
      if (this._isDisabled())
        return null;

      if (event.type == "ojfocusout")
      {
        this._findFirstFocusableInHeader()
          .attr("tabIndex", -1);

        event.preventDefault();
        event.stopPropagation();
      }
      else if (event.type == "ojfocus")
      {
        this._findFirstFocusableInHeader()
          .attr("tabIndex", 0)
          .focus();
        event.preventDefault();
        event.stopPropagation();
      }
    },

    _findFirstFocusableInHeader : function ()
    {
      return this._findFocusables(this.header).first();
    },

    _findFocusables : function (start)
    {
      //create <span> or <a> depending on if this.isDisabled
      if (this._isDisabled()) {
        return start.find("span");
      }
      return start.find("a,:input");
    },

    /**
     * Expand a collapsible.<p>
     * Note the beforeExpand event will only be fired when vetoable is true.<p>
     * Please use the <code class="prettyprint">expanded</code> option 
     * for expanding a collapsible so that it triggers the beforeExpand event:
     * $( ".selector" ).ojCollapsible( "option", "expanded", true );
     * 
     * @expose 
     * @memberof oj.ojCollapsible
     * @instance
     * @param {boolean} vetoable if event is vetoable
     */
    expand : function (vetoable, event)
    {
      if (this._isDisabled())
        return;

      var eventData = 
      {
        /** @expose */
        header : this.header, 
        /** @expose */
        content : this.content
      };

      if (!vetoable || this._trigger("beforeExpand", event, eventData) !== false)
      {
        this._trigger("expand", event, eventData);
        //fire option change event in _expandCollapseHandler
//        this._changeExpandedOption(true, event);
      }
    },

    /**
     * Collapse a collapsible.<p>
     * Note the beforeCollapse event will only be fired when vetoable is true.<p>
     * Please use the <code class="prettyprint">expanded</code> option 
     * for collapsing a collapsible so that it triggers the beforeCollapse event:
     * $( ".selector" ).ojCollapsible( "option", "expanded", false );
     * 
     * @expose 
     * @memberof oj.ojCollapsible
     * @instance
     * @param {boolean} vetoable if event is vetoable
     */
    collapse : function (vetoable, event)
    {
      if (this._isDisabled())
        return;

      var eventData = 
      {
        /** @expose */
        header : this.header, 
        /** @expose */
        content : this.content
      };

      if (!vetoable || this._trigger("beforeCollapse", event, eventData) !== false)
      {
        this._trigger("collapse", event, eventData);
        //fire option change event in _expandCollapseHandler
//        this._changeExpandedOption(false, event);
      }
    },

    _transitionEndHandler : function (event)
    {
      if (this._isDisabled())
        return;

      var propName = event.originalEvent? event.originalEvent.propertyName : null;

      //TODO: fire expand and collapse here
      if (propName == "max-height")
      {
        event.preventDefault();
        event.stopPropagation();
      }

      //just completed a collapse transition
      if (this.options.expanded)
      {
        this.wrapper.css({
          'max-height': 9999,
          'overflow-y': ''
        });
      }
      else 
      {
        //James: set display:none on the wrapper when it is hidden and then remove display:none when its is shown. 
        //This should trigger JAWS into refreshing the buffer.
        this.wrapper.hide();
      }

      this.wrapper.removeClass("oj-collapsible-transition");
    },

    /**
     * @param {boolean} value
     * @param {Event} originalEvent  
     *
     * @private
     */
    _changeExpandedOption: function(value, originalEvent) 
    {
      this.option('expanded', value, {'_context': {originalEvent: originalEvent,
                                                   writeback: true, 
                                                   internalSet: true}});
    },

    /**
     * Return the subcomponent node represented by the documented locator 
     * attribute values.
     *
     * Test authors should target sub elements using the following names:
     * <ul>
     * <li><b>oj-collapsible-header</b>: collapsible header </li>
     * <li><b>oj-collapsible-content</b>: collapsible content </li>
     * <li><b>oj-collapsible-header-icon</b>: collapsible disclosure icon </li>
     * </ul>
     *
     * @expose
     * @memberof! oj.ojCollapsible
     * @instance
     * @override
     * @param {Object} locator An Object containing at minimum a subId property 
     *        whose value is a string, documented by the component, that allows 
     *        the component to look up the subcomponent associated with that 
     *        string.  It contains:<p>
     *        component: optional - in the future there may be more than one 
     *        component contained within a page element<p>
     *        subId: the string, documented by the component, that the component 
     *        expects in getNodeBySubId to locate a particular subcomponent
     * @returns {Element|null} the subcomponent located by the subId string passed
     *          in locator, if found.<p>
     */
    getNodeBySubId: function(locator)
    {
      if (locator == null)
      {
        return this.element ? this.element[0] : null;
      }

      var subId = locator['subId'];

      switch (subId)
      {
      case 'oj-collapsible-content':
        return this.content[0];

      case 'oj-collapsible-header':
        return this.header[0];

      case 'oj-collapsible-header-icon':
        return this.header.find("." + subId)[0];
      }

      // Non-null locators have to be handled by the component subclasses
      return null;
    }

    // Fragments:

    /**
     * <table class="keyboard-table">
     *   <thead>
     *     <tr>
     *       <th>Target</th>
     *       <th>Gesture</th>
     *       <th>Action</th>
     *     </tr>
     *   </thead>
     *   <tbody>
     *     <tr>
     *       <td>Header</td>
     *       <td><kbd>Tap</kbd></td>
     *       <td>Toggle disclosure state</td>
     *     </tr>
     *   </tbody>
     * </table>
     *
     * @ojfragment touchDoc - Used in touch gesture section of classdesc, and standalone gesture doc
     * @memberof oj.ojCollapsible
     */
        
    /**
     * <table class="keyboard-table">
     *   <thead>
     *     <tr>
     *       <th>Target</th>
     *       <th>Key</th>
     *       <th>Action</th>
     *     </tr>
     *   </thead>
     *   <tbody>
     *     <tr>
     *       <td>Header</td>
     *       <td><kbd>Space or Enter</kbd></td>
     *       <td>Toggle disclosure state.</tr>
     *     </tr>
     *   </tbody>
     * </table>
     *
     * <p>Disabled items can receive keyboard focus, but do not allow any other interaction.
     *
     * @ojfragment keyboardDoc - Used in keyboard section of classdesc, and standalone gesture doc
     * @memberof oj.ojCollapsible
     */

  });

}
());