Problem:
When you disable a header of an accordion by means of getHeaderAt(index).enabled the navigation is disabled for the mouse (you cant click to the tab) but if you navigate with the keyboard (using arrows or pg up/ pg down) you can still navigate into the disabled pages of the accordion.

I show you here an approximation to the problem, extending the accordion to make an override to the keyDownHandler method and making functionality of our own to avoid this.

Original issue came from a requirement, and ideas came from Peter Ent’s Blog Entry, you’ll find a post with this code there, but few changes were made to fix some issues.

You will also see in my examples some images on my custom preloader, you can read more on preloaders in this Ted Patrick’s Article.
J0cks has been kind enough to let me use his art for this, in this article you’ll see Blue Skyline

Solution:
I extended the accordion and made an override to the keyDownHandler method and broke the process in two functions the keyDownHandler and getIndexPlus this second one is a recursive function that will browse all available pages and check for the first one with the header enabled, so it will be the next target page.

Known issues:
None at the moment.

Working Example

To see the live example, click here please.

Here is the code of the Accordion..

package components {
import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.ui.Keyboard;
import mx.containers.Accordion;
import mx.events.IndexChangedEvent;
public class PracticalAccordion extends Accordion {
     private var _focusedIndex:int = -1;
     private var mySelectedIndex:int = -1;
     public function PracticalAccordion() {
          super();
     }
     private function drawHeaderFocus(headerIndex:int, isFocused:Boolean):void {
          if (headerIndex >= 0 && headerIndex < numChildren)
               getHeaderAt(headerIndex).drawFocus(isFocused);
     }
/**
* for readability sake i broke this into two parts from the original implementation
* of the Accordion.
* Here i disable the drawfocus when relevant (so tab wont be focused if it isnt the
* one that should be focused)
* Another part of the original implementation its in the recursive function
* getIndexPlus and finally dispatch we call the dispatchChangeEvent to make
* the visual Changes.
*/
     override protected function keyDownHandler(event:KeyboardEvent):void {
          if (event.target != this)
               return;
          var prevValue:int = selectedIndex;
          if(event.keyCode == Keyboard.SPACE || event.keyCode == Keyboard.ENTER
               || event.keyCode == Keyboard.END || event.keyCode == Keyboard.HOME 
               || event.keyCode == Keyboard.DOWN || event.keyCode == Keyboard.UP 
               || event.keyCode == Keyboard.LEFT || event.keyCode == Keyboard.RIGHT 
               || event.keyCode == Keyboard.PAGE_DOWN || event.keyCode == Keyboard.PAGE_UP)  {
                    if(isNavigationEnabled()) {
                         if(event.keyCode != Keyboard.SPACE && event.keyCode != Keyboard.ENTER)
                              drawHeaderFocus(_focusedIndex, false);
                         mySelectedIndex = selectedIndex;
                         getIndexPlus(event, 0, prevValue);
                         selectedIndex = mySelectedIndex;
                         if(event.keyCode != Keyboard.SPACE && event.keyCode != Keyboard.ENTER)
                              drawHeaderFocus(_focusedIndex, true);
                         event.stopPropagation();
                         if(event.keyCode != Keyboard.DOWN && event.keyCode != Keyboard.UP 
                              && event.keyCode != Keyboard.LEFT && event.keyCode != Keyboard.RIGHT)
                                   dispatchChangeEvent(prevValue, selectedIndex, event);
                    }
          }
     }
/**
* This function will see if headers are enabled, if there is at least one header enabled, navigation will funcion normally.
*/
     private function isNavigationEnabled():Boolean {
          var ok:Boolean = false;
          for(var i:int = 0; i < numChildren; i++)
               if(getHeaderAt(i).enabled)
                    ok = ok || true;
          return ok;
     }
     private function lastEnabledIndex():int {
          for(var i:int = numChildren - 1; i >= 0; i--)
               if(getHeaderAt(i).enabled)
                    return i;
          return numChildren - 1;
     }
     private function firstEnabledIndex():int {
          for(var i:int = 0; i < numChildren; i++)
               if(getHeaderAt(i).enabled)
                    return i;
          return 0;
     }
/**
* This is my recursive function that will set the index the same way the
* traditional accordion does, the only difference is it will check
* if the header is enabled for the purposed next page, if it is it will set that
* as selectedIndex, if it isnt, it will seek the next available
* page to be rendered.
*/
     private function getIndexPlus (event:KeyboardEvent, loop:int, prevValue:int):void {
          switch (event.keyCode) {
               case Keyboard.PAGE_DOWN: {
                    _focusedIndex = mySelectedIndex = (mySelectedIndex < numChildren - 1 ? 
                              mySelectedIndex + 1 : 0);
                    break;
               }
               case Keyboard.PAGE_UP: {
                    _focusedIndex = mySelectedIndex = (mySelectedIndex > 0 ? 
                         mySelectedIndex - 1 : numChildren - 1);
                    break;
               }
               case Keyboard.HOME: {
                    _focusedIndex = mySelectedIndex = firstEnabledIndex();
                    break;
               }
               case Keyboard.END: {
                    _focusedIndex = mySelectedIndex = lastEnabledIndex();
                    break;
               }
               case Keyboard.DOWN:
               case Keyboard.RIGHT: {
                    _focusedIndex = (_focusedIndex < numChildren - 1 ? _focusedIndex + 1 : 0);
                    break;
               }
               case Keyboard.UP:
               case Keyboard.LEFT: {
                    _focusedIndex = (_focusedIndex > 0 ? _focusedIndex - 1 : numChildren - 1);
                    break;
               }
               case Keyboard.SPACE:
               case Keyboard.ENTER: {
                    if (_focusedIndex != mySelectedIndex) {
                         mySelectedIndex = _focusedIndex;
                    }
                    break;
               }
          }
          if (_focusedIndex != mySelectedIndex) {
          /* here we check if the header is enabled*/
               if(!getHeaderAt(mySelectedIndex).enabled || !getHeaderAt(_focusedIndex).enabled)
/* this is important as you know recursive functions require a state base to exit the loop, here is when the state is reached */
                    if(loop < numChildren)
                         getIndexPlus(event, loop + 1, prevValue);
                    else 
                         _focusedIndex = mySelectedIndex = prevValue;
          } else
               if(!getHeaderAt(mySelectedIndex).enabled)
/* this is important as you know recursive functions require a state base to exit the loop, here is when the state is reached */
                    if(loop < numChildren)
                         getIndexPlus(event, loop + 1, prevValue);
                    else
                         _focusedIndex = mySelectedIndex = prevValue;
     }
/**
* We need to dispatch the event of the change for the accordion to make the change visually
*/
     private function dispatchChangeEvent(oldIndex:int, newIndex:int, cause:Event = null):void {
          var indexChangeEvent:IndexChangedEvent = 
                    new IndexChangedEvent(IndexChangedEvent.CHANGE);
          indexChangeEvent.oldIndex = oldIndex;
          indexChangeEvent.newIndex = newIndex;
          indexChangeEvent.relatedObject = getChildAt(newIndex);
          indexChangeEvent.triggerEvent = cause;
          dispatchEvent(indexChangeEvent);
     }
  }
}

One Response to “Flex – [Accordion] Disabling navigation on headers.”

Leave a Reply