Flex ItemRenderer prevents use of tabbing between text inputs
Asked Answered
S

9

7

I have a custom ItemRenderer that displays 5 text inputs in each of 3 panels:

<?xml version="1.0" encoding="utf-8"?>
<mx:VBox 
    xmlns:mx="http://www.adobe.com/2006/mxml"
    height="300"
    width="800"
    creationComplete="onCreationComplete()"
>
    <!-- code-behind -->
    <mx:Script source="ChainListRenderer.mxml.as" />

    <mx:Label text="{data.title}" fontSize="25" fontWeight="bold" width="100%" textAlign="center" />
    <mx:HBox>
        <mx:Panel id="triggerPanel" title="Trigger" width="260">
            <mx:VBox id="tpBoxes" width="100%" paddingBottom="5" paddingLeft="5" paddingRight="5" paddingTop="5">
                <mx:TextInput id="trigger1" width="100%" textAlign="left" tabIndex="0" tabEnabled="true" />
                <mx:TextInput id="trigger2" width="100%" textAlign="left" tabIndex="1" tabEnabled="true" />
                <mx:TextInput id="trigger3" width="100%" textAlign="left" tabIndex="2" tabEnabled="true" />
                <mx:TextInput id="trigger4" width="100%" textAlign="left" tabIndex="3" tabEnabled="true" />
                <mx:TextInput id="trigger5" width="100%" textAlign="left" tabIndex="4" tabEnabled="true" />
            </mx:VBox>
        </mx:Panel>
        <mx:Panel id="linkPanel" title="Link" width="260">
            <mx:VBox id="lpBoxes" width="100%" paddingBottom="5" paddingLeft="5" paddingRight="5" paddingTop="5">
                <mx:TextInput id="link1" width="100%" textAlign="left" tabIndex="5" tabEnabled="true" />
                <mx:TextInput id="link2" width="100%" textAlign="left" tabIndex="6" tabEnabled="true" />
                <mx:TextInput id="link3" width="100%" textAlign="left" tabIndex="7" tabEnabled="true" />
                <mx:TextInput id="link4" width="100%" textAlign="left" tabIndex="8" tabEnabled="true" />
                <mx:TextInput id="link5" width="100%" textAlign="left" tabIndex="9" tabEnabled="true" />
            </mx:VBox>
        </mx:Panel>
        <mx:Panel id="answerPanel" title="Answer" width="260">
            <mx:VBox id="apBoxes" width="100%" paddingBottom="5" paddingLeft="5" paddingRight="5" paddingTop="5">
                <mx:TextInput id="answer1" width="100%" textAlign="left" tabIndex="10" tabEnabled="true" />
                <mx:TextInput id="answer2" width="100%" textAlign="left" tabIndex="11" tabEnabled="true" />
                <mx:TextInput id="answer3" width="100%" textAlign="left" tabIndex="12" tabEnabled="true" />
                <mx:TextInput id="answer4" width="100%" textAlign="left" tabIndex="13" tabEnabled="true" />
                <mx:TextInput id="answer5" width="100%" textAlign="left" tabIndex="14" tabEnabled="true" />
            </mx:VBox>
        </mx:Panel>
    </mx:HBox>
</mx:VBox>

Unfortunately, when used as an ItemRenderer, tabbing between the text inputs doesn't work, even with the tabIndex values above. If I copy this code to an MXML application of its own, tabbing between text inputs works as expected.

Does anyone know how to restore tabbing in this scenario? It will be a shame if I have to release this app without such a simple usability element.

I suppose I may need to implement mx.managers.IFocusManagerComponent, but I can't find any examples on how to do that, and the FocusManager docs aren't helping either.

Steamtight answered 18/5, 2009 at 17:56 Comment(4)
Did you try setting tabEnabled on the textInputs?Prefect
I tried that just now, it doesn't seem to help.Steamtight
When you say the "tab indexing doesn't work" what behavior are you seeing? Does it jump to the next control after your List-based component or is it tabbing around the browser chrome?Pita
The ItemRenderer above is used in a HorizontalList control. When one of the TextInput controls above has focus and I hit the tab key, the focus jumps outside of the HorizontalList control and to the next control on the application's Canvas. Hitting tab a few more times does cycle up through the browser controls like the address bar.Steamtight
D
3

I was using an mx:VBox as a custom itemRenderer with rendererIsEditor="true" for my datagrid, and I was running into the tab order issue as well.

What I figured out is that the itemRenderer needs to implement IFocusManagerComponent in order for the main application's FocusManager() to be able to tab correctly to it. I tried implementing that interface:

<?xml version="1.0"?>
<mx:VBox implements="mx.managers.IFocusManagerComponent" ...>
 [the rest of my itemRenderer code]
</mx:VBox>

...and it turns out to be rather complex to do...lots of interface functions to implement.

However in my case I was lucky; I only had one TextInput element in my itemRenderer (the rest was all just custom code, validators & formatters) so I converted my itemRenderer from mx:VBox to mx:TextInput (which already implements the IFocusManagerComponent):

<?xml version="1.0"?>
<mx:TextInput ...>
 [the rest of my itemRenderer code]
</mx:TextInput>

Voila! My tab order issue was fixed.

I suppose the conclusion for those of you with more complex itemRenderers is you'll need to either fully implement the IFocusManagerComponent interface in your class...which is probably good because it looks like it would tell flex how to custom-tab through your itemRenderer fields. Or perhaps you could change your top level tag to something which already implements the interface, eg: could you nest the mx:VBox inside something like:

<mx:Container focusIn="FocusManager.setFocus(trigger1)">

...and have it work perhaps? Someone with more complex code than I should give it a try and see what happens.

Delisle answered 3/9, 2009 at 0:4 Comment(2)
Hi. For what it's worth, UIComponent implements all the required methods of IFocusManagerComponent, but just not the interface (because some UIComponents aren't supposed to get focus). So, to make a component focus-able, you simply add the interface declaration (no need to implement any of the methods - UIComponent does that for you. adobe.com/livedocs/flex/201/langref/mx/managers/…Fiery
Sinc the URL provided in the previous comment is broken, here's an alternative/updated URL: IFocusManagerComponent.Oldtime
A
3

I ran into the same problem with an itemRender used in a "ListBase derived" component. I found that all "ListBase derived" components wrap all the item rendereres in a ListBaseContentHolder.

From the ListBase source:

/**
 *  An internal display object that parents all of the item renderers,
 *  selection and highlighting indicators and other supporting graphics.
 *  This is roughly equivalent to the <code>contentPane</code> in the 
 *  Container class, and is used for managing scrolling.
 */
protected var listContent:ListBaseContentHolder;

The tabChildren and tabEnabled properties of this class are set to false by default. The only workaround I could find was to create a MyList component deriving from the List and override the createChildren method (where the listContent is initialized) this way:

import flash.display.DisplayObject;
import mx.controls.List;

public class MyList extends List {
    override protected function createChildren():void {
            super.createChildren();
            this.listContent.tabChildren = this.tabChildren
            this.listContent.tabEnabled = this.tabEnabled
        }
    }

Then using "<MyList tabChildren="true"/>" instead of the "<mx:List/>" component gave me back the tabbing functionnality in the ItemRender.

Hope it helps,

Adallard answered 11/3, 2010 at 12:34 Comment(0)
S
0

I think I may be moving in the right direction, but I'm not entirely there yet.

I have my main application, with the HorizontalList using a custom ItemRenderer:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application 
    xmlns:mx="http://www.adobe.com/2006/mxml" 
    xmlns:com="ventures.view.component.*"
    layout="absolute"
    backgroundColor="#ffffff"
    preinitialize="onPreInitialize()"
    click="onClick(event)"
>
    <!-- code-behind -->
    <mx:Script source="ConsumptionChain.as" />

    <!-- show chain links -->
    <mx:HorizontalList
        id="ChainList"
        direction="horizontal"
        dataProvider="{chainData}"
        rollOverColor="#ffffff"
        selectionColor="#ffffff"
        horizontalScrollPolicy="off"
        left="20"
        right="20"
        top="20"
        height="300"
        minWidth="802"
        rowHeight="300"
        columnWidth="800"
        tabChildren="false"
        itemRenderer="ventures.view.ItemRenderer.ChainListRenderer"
    />

</mx:Application>

I've updated the code sample in the original question to contain the entire ItemRenderer MXML (applicable portions); and here's the ActionScript code-behind:

/*
 * ChainListRenderer.mxml.as -- code-behind for ChainListRenderer.mxml
 */

import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.ui.Keyboard;

//used to combine all textboxes in a single array to make looping through them with the TAB key easier
private var allBoxes:Array;

public function expandPanel(e:Event):void {
    trace(e.currentTarget);                
    var state : String = e.currentTarget.parent.parent.id;                
    if (state != this.currentState)
        this.currentState = state;
}

private function onCreationComplete():void{
    //this function will be run on each object via the map function
    function forEach(o:Object, index:int, ar:Array):void{
        o.addEventListener(FocusEvent.FOCUS_IN, expandPanel)
        o.addEventListener(MouseEvent.CLICK, stopBubble);           //don't propagate click events (which return to base state)
        o.addEventListener(KeyboardEvent.KEY_DOWN, customTabbing);  //fix tabbing between text fields
    }

    this.addEventListener(KeyboardEvent.KEY_DOWN, customTabbing);

    //loop over textboxes and add the focusIn event listener
    tpBoxes.getChildren().map(forEach);
    lpBoxes.getChildren().map(forEach);
    apBoxes.getChildren().map(forEach);

    //create an "allBoxes" array that is used by the customTabbing function
    allBoxes = tpBoxes.getChildren();
    function forEachTabbing(o:Object, index:int, ar:Array):void {
        allBoxes.splice(allBoxes.length, 0, o);
    }
    lpBoxes.getChildren().map(forEachTabbing);
    apBoxes.getChildren().map(forEachTabbing);
}

//this function is used to prevent event bubbling
private function stopBubble(e:Event):void {
    e.stopPropagation();
}

//this function re-implements tabbing between text fields, which is broken when inside an itemRenderer
public function customTabbing(e:KeyboardEvent):void {
    trace('keyCode: ' && e.keyCode.toString());
    if (e.keyCode == flash.ui.Keyboard.TAB){
        trace(focusManager.getFocus());
        //loop over array of all text-boxes
        for (var i:int = 0; i < allBoxes.length; i++){
            trace(i.toString());
            //if event (keypress) current target is the current TextInput from the array
            if (e.currentTarget == allBoxes[i]){
                //then focus the NEXT TextInput, and wrap if at last one
                allBoxes[((i+1) % allBoxes.length)].setFocus();
                //break out of the loop
                break;
            }
        }

        //prevent arrow keys from navigating the horizontal list
        e.stopPropagation();
    }
}

Essentially, what I'm attempting to do here is re-implement tabbing by checking for the TAB key on key_down event of each text field, and using that to move focus to the next TextInput (wrapping to the first TextInput from the last). This doesn't have the desired effect though, and the focus still jumps out to the next control outside of this HorizontalList.

Any ideas where to go from here?

Steamtight answered 19/5, 2009 at 20:1 Comment(0)
L
0

Why are you doing tabChildren="false"? Don't you want to tab the children of the HorziontalList? since the itemRenderer is a child of the list....

Leahy answered 3/6, 2009 at 18:22 Comment(1)
I wasn't originally. It was something I tried out of desperation, and didn't seem to have any effect, so it got left in. I've abandoned the whole HorizontalList with a custom itemRenderer approach, though, and just gone for a custom component, which has enabled tabbing. I'm leaving the question up so that others who run into the same issue can at least find what I've found so far.Steamtight
S
0

This just doesn't seem to be possible with the method I was using. Instead of using a list with a custom Item Renderer I switched to a single-item view component and a separate list to show a summary of all items, and this let me fix my problem.

Steamtight answered 24/8, 2009 at 13:4 Comment(0)
M
0

I had the same problem, solved it by trying to reimplement the behavior of the tab button. The clue to success is simply using the event.preventdefault() method. The code used is shown ahead.

private function initFocusMap():void {
    focusMap = {
        InNom:benefPersona.InApePat,
        InApePat:benefPersona.InApeMat,
        InApeMat:benefPersona.InFecNacimiento,
        InFecNacimiento:benefPersona.InRFC,
        InRFC:benefPersona.InCURP,
        InCURP:benefPersona.InIdSexo,
        InIdSexo:benefPersona.InIdParentesco,
        InIdParentesco:benefPersona.InPorc,
        InPorc:domBeneficiario.InCalle,
        InCalle:domBeneficiario.InNumExterior,
        InNumExterior:domBeneficiario.InNumInterior,
        InNumInterior:domBeneficiario.InCP,
        InCP:domBeneficiario.InColonia,
        InColonia:domBeneficiario.InIdPais,
        InIdPais:domBeneficiario.InIdEdo,
        InIdEdo:domBeneficiario.InIdCiudad,
        InIdCiudad:benefPersona.InNom                   
    }
}

private function kfcHandler(event:FocusEvent):void {
    var id:String = ""
    if (event.target is AperClsDateField || event.target is AperClsCombo) {
        id = event.target.id;
    } else {
        id = event.target.parent.id;
    }
    if (id != "InIdDelegacionMunic") {
        event.preventDefault();             
        focusManager.setFocus(focusMap[id]);
    }
}
Mathias answered 27/12, 2009 at 1:30 Comment(0)
C
0

This is how it's done with a ComboBox itemEditor:

http://blog.flexmonkeypatches.com/2008/02/18/simple-datagrid-combobox-as-item-editor-example/

Commonalty answered 28/5, 2010 at 18:5 Comment(0)
B
0

I was having the same issues of tabbing one of the itemRenderer in my AdvancedDataGrid (by the way, I am using Flex SDK 3.5) but this post has been extremely helpful in allowing me to make my tab-friendly itemRenderer so I would like to make a contribution as well :)

To make this to work, you also need to change a few properties on the grid and gridColumn.

Let's 1st talk about the grid and gridColumn.

As you all know when you set the grid's "editable" property to "true", you could tab through each column cell (assuming you didn't set the column's "editable" property to "false").

Step #1, make your grid's "editable" property sets to "true"

Step #2, make your grid's column "editable" property also sets to "true" and "rendererIsEditor" to "true"

It's important to set the dataField to a bogus field because since we set the renderer as editor that means whatever you update in the itemRenderer will be assigned to the dataField i.e. you set the dataField to "Foo" which has a type int and you have non-primitive objects populating a comboBox itemRenderer. When you make a selection, that object will be assigned to "Foo"

Step #3, make your grid's column "dataField" property also sets to a bogus field.

Now let's do the thing that enable tabbing to work on itemRenderer

I know that this is not an optimized version but this will do for the 1st pass.

import mx.core.mx_internal;
import mx.managers.IFocusManagerComponent;
use namespace mx_internal;

public class FriendlyItemRendererContainer extends HBox implements IFocusManagerComponent
{

    public function FriendlyItemRendererContainer ()
    {
    super();
    addEventListener(FocusEvent.KEY_FOCUS_CHANGE, keyFocusChangeHandler);       
    }

    private var _listData:DataGridListData;
    //This is required to make it work as itemEditor
    public var text:String;

    private function keyFocusChangeHandler(event:FocusEvent):void
    {
            if (event.keyCode == Keyboard.TAB &&
                ! event.isDefaultPrevented() &&
                findNextChildToFocus(event.shiftKey))
            {

                event.preventDefault();

            }

    }

    private function findNextChildToFocus(shiftKey:Boolean):Boolean
    {
          var myChildrenAry:Array = getChildren();
      var incr:int = shiftKey ? -1 : 1;
      var index:int = shiftKey ? myChildrenAry.length : 0;
      var focusChildIndex:int = 0;
      var found:Boolean = false;

         for (focusChildIndex = 0; focusChildIndex < myChildrenAry.length; ++focusChildIndex)
     {
        if (!(myChildrenAry[focusChildIndex] as UIComponent).visible ||
            (myChildrenAry[focusChildIndex] is Container))
        {
            //If it's an invisible UIComponent or a container then just continue
            continue;
        }

            if (myChildrenAry[focusChildIndex] is TextInput)
        {
                    if (systemManager.stage.focus == (myChildrenAry[focusChildIndex] as TextInput).getTextField())
                    {
                        (myChildrenAry[focusChildIndex] as UIComponent).drawFocus(false);
                        found = true;
                        break;
                    }
        }
        else
        {
                    if (systemManager.stage.focus == myChildrenAry[focusChildIndex])
                    {
                        (myChildrenAry[focusChildIndex] as UIComponent).drawFocus(false);
                        found = true;
                        break;
                    }
       }
         }

         if (!found)
     {
        focusChildIndex = 0;
      }

      while (true)
      {
                focusChildIndex = focusChildIndex + incr;

                if ((focusChildIndex < 0) || (focusChildIndex >= myChildrenAry.length))
                {
                    UIComponentGlobals.nextFocusObject = null;
                    return false;
                }
                else
                if (myChildrenAry[focusChildIndex] is UIComponent)
                {
                (myChildrenAry[focusChildIndex] as UIComponent).setFocus();
                (myChildrenAry[focusChildIndex] as UIComponent).drawFocus(true);

                    break;
                }
        }


        return true;
    }

    override public function setFocus():void
    {
       var myChildrenAry:Array = getChildren();
       if (myChildrenAry.length > 0)
       {
        for (var i:int = 0; i < myChildrenAry.length; ++i)
        {
            if ((myChildrenAry[i] is UIComponent) && (myChildrenAry[i] as UIComponent).visible)
            {
               (myChildrenAry[i] as UIComponent).setFocus();
                   (myChildrenAry[i] as UIComponent).drawFocus(true);
               break;
            }
       }
    }

    }

    public function get listData():BaseListData
    {
            return _listData;
    }

    public function set listData(value:BaseListData):void
    {
            _listData = DataGridListData(value);
    }  
}

Example on how to use it on your itemRenderer:

<FriendlyItemRendererContainer xmlns:mx="http://www.adobe.com/2006/mxml">

<mx:TextInput/>
<mx:Button label="Boo"/>
<mx:RadioButton/>
<<mx:TextInput/>
<mx:Button label="Baa"/>

</FriendlyItemRendererContainer>

Then just put that in the gridColumn and that's it.

Enjoy.

Burnard answered 27/4, 2011 at 15:43 Comment(0)
C
0

Basically you want to remove the default behavior of focus change event. I think you need to do this:

1. yourRenderer.addEventListener(FocusEvent.KEY_FOCUS_CHANGE, onKeyFocusChange);
2. since you want to stop tab key, in the handler, do this: 
        if (event.keyCode == Keyboard.TAB)
            event.preventDefault()
3. in your keyDown handler, catch TAB, then you can manually move your focus.
Cambrai answered 15/11, 2011 at 14:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.