Flex component developers need to be able to provide their own accessibility implementations for custom components. The Flash Player provides the flash.accessibility.AccessibilityImplementation class as a base class for creating custom accessibility implementations for Flex and Flash UI components in ActionScript 3. In Flex, this class is extended by mx.accessibility.AccImpl which serves as the base class for accessibility implementations in Flex components. A new accessibility implementation can be created by extending mx.accessibility.AccImpl for each new component.
The methods of the flash.accessibility.AccessibilityImplementation class are a subset of the IAccessible interface, adapted slightly to make life easier. The Flash Player's IAccessible interface for a scripted movie clip simply passes most calls from MSAA through to the flash.accessibility.AccessibilityImplementation subclass instance for that component.
Accessibility implementations must also send event notifications to MSAA in a variety of situations. This is accomplished by using the flash.accessibility.Accessibility.sendEvent() method.
The way in which an accessibility implementation implements the IAccessible interface and the events that it sends depend on the kind of component being implemented. There are two primary forms of guidance here. First, the MSAA documentation has a list of guidelines for accessible object implementations according to component type. Second, an accessibility implementations's behavior should match as closely as possible the behavior exhibited by the equivalent form element (if any such element exists) in an HTML page inside Internet Explorer or Firefox or in the Windows operating system. This behavior can be examined using the AccExplorer, Inspect32, and AccEvent tools, available in the Microsoft Active Accessibility 2.0 SDK. Collectively, these two forms of guidance can be said to constitute an MSAA model for IAccessible implementations. It is, unfortunately, a rather loosely specified model, and the only definitive test of an IAccessible implementation is to hit it with shipping screen readers.
This article will now explain how to define a custom accessibility implementation for a component. For this example, we'll add an accessibility implementation to the mx.controls.PopUpButton and, by extension, the mx.controls.PopUpMenuButton class.
For an overview of the PopUpButton and PopUpMenuButton controls see the articles PopUpButton control and PopUpMenuButton control in the Flex 3 Developer's Guide and mx.controls.PopUpButton and mx.controls.PopUpMenuButton in the Adobe Flex 3 Language Reference.
Create a new Flex project named “PopUpMenuButton.” Be sure to enable accessibility in the project using one of the methods explained in Configuring Flex applications for accessibility.
Defining a custom accessiblity implementation for a component requires changes the component's class file.
Copy the PopUpButton class file from its location in the Flex 3 SDK source code, /sdks/3.0.0/frameworks/projects/framework/src/mx/controls/PopUpButton.as to a directory in the source folder for the project, /mx/controls/PopUpButton.as. You'll also need to copy the included files /sdks/3.0.0/frameworks/projects/framework/src/mx/core/Version.as to /mx/core/Version.as and /sdks/3.0.0/frameworks/projects/framework/src/mx/styles/metadata/IconColorStyles.as to /mx/styles/metadata/IconColorStyles.as so that the project compiles without errors. When the project is compiled, the Flex compiler will override the existing PopUpButton class contained within the Flex framework, with the modified class in project folder, which will support accessibility.
Open the PopUpButton class file that you just copied over from the Flex SDK. At line 168 in the code, add the following meta tag:
[AccessibilityClass(implementation="mx.accessibility.PopUpButtonAccImpl")]
The [AccessibilityClass] meta tag will let the compiler know which class will serve as the accessibility implementation for this component.
Add the following code to create a placeholder for the static createAccessibilityImplementation() method at line 244, just before the PopUpButton constructor:
… //-------------------------------------------------------------------------- // // Class mixins // //-------------------------------------------------------------------------- /** * Placeholder for mixin by PopUpButtonAccImpl. */ mx_internal static var createAccessibilityImplementation:Function; …
This createAccessibilityImplementation() method will be assigned by the PopUpButtonAccImpl accessibility implementation class we'll create.
At line 469 among the overridden methods from UIComponent class, add the following initializeAccessibility() method:
…
/**
* @inheritDoc
*/
override protected function initializeAccessibility():void
{
if (PopUpButton.createAccessibilityImplementation != null)
PopUpButton.createAccessibilityImplementation(this);
}
…
The initializeAccessibility() method is invoked by the UIComponent.initialize() method to initialize accessibility for a component instance at runtime.
To define the PopUpButtonAccImpl class, copy the ButtonAccImpl class file from its location in the Flex 3 SDK source code, /sdks/3.0.0/frameworks/projects/framework/src/mx/accessibility/Button.as to a directory in the source folder for the project with a new file name, /mx/accessibility/PopUpButtonAccImpl.as.
Open the PopUpButtonAccImpl class file that you just copied over from ButtonAccImpl class in the Flex SDK. Update the import statement block to import the PopUpButton, Menu, AccessibilityProperties and Rectangle classes:
…
package mx.accessibility
{
import flash.accessibility.Accessibility;
import flash.accessibility.AccessibilityProperties;
import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.geom.Rectangle;
import flash.ui.Keyboard;
import mx.controls.Menu;
import mx.controls.PopUpButton;
import mx.core.UIComponent;
import mx.core.mx_internal;
…
At line 61 in the code, update the class definition to read:
…
public class PopUpButtonAccImpl extends AccImpl
…
At line 93 in the code update the hookAccessibility method to read:
…
private static function hookAccessibility():Boolean
{
PopUpButton.createAccessibilityImplementation =
createAccessibilityImplementation;
return true;
}
…
The hookAccessibility method defines the PopUpButton class' static createAccessibilityImplementation to use the method of the same name defined in the PopUpButtonAccImpl class. It will get called once at runtime when the PopUpButtonAccImpl class initializes and defines the static accessibilityHooked property.
Replace the createAccessibilityImplementation method at line 149 with the following:
…
mx_internal static function createAccessibilityImplementation(
component:UIComponent):void
{
component.accessibilityImplementation =
new PopUpButtonAccImpl(component);
}
…
The createAccessibilityImplementation method assigns a new PopUpButtonAccImpl instance to the accessibilityImplementation property of a given component instance.
Add the following four static state and role constants at line 122:
…
/**
* The object is hot-tracked by the mouse, which means that its appearance has changed to indicate that the mouse pointer is located over it.
*/
private static const STATE_SYSTEM_HOTTRACKED:uint = 0x00000080;
/**
* Object displays a pop-up menu or window when invoked.
*/
private static const STATE_SYSTEM_HASPOPUP:uint = 0x40000000;
/**
* The role represents a button that has a drop-down list icon directly adjacent to the button.
*/
private static const ROLE_SYSTEM_SPLITBUTTON:uint = 0x3e;
/**
* The object represents a button that drops down a list of items.
*/
private static const ROLE_SYSTEM_BUTTONDROPDOWN:uint = 0x38;
…
System roles and states are predefined for all components in MSAA. The hexidecimal values for the constants can be found in the language reference under AccessibilityImplementation Constants. The MSAA documentation has a list of guidelines for accessible object implementations that documents which constants are used by which component types.
Unfortunately, the User Interface Element Reference for MSAA omits the pop-up button. In order to figure out the appropriate role and state constants for the PopUpButton Accessibility Implementation, use the AccExplorer, Inspect32, and AccEvent tools available in the Microsoft Active Accessibility 2.0 SDK to examine a similar existing component in Windows. Fortunately, the Flex Builder 3 IDE and Eclipse plug-in use the pop-up button for a number of items in the main toolbar. Launch Inspect32. Mouse over the Run icon button in the Flex Builder 3 main toolbar. The Inspect32 tool should update to reveal the following information:
How found: Mouse move (199,85)
hwnd=0x0006038A 32bit class="ToolbarWindow32" style=0x5601A945 ex=0x100000
Info: IAcc = 0x001F7A6C VarChild:[VT_I4=0x0]
Interfaces: IEnumVARIANT IOleWindow IAccIdentity
Impl: Remote oleacc proxy
Annotation ID: 0100008000000000FCFFFFFF00000000
Name: "Run PopUpMenuButton"
Value: none [false]
Role: split button
State: hot tracked
Location: {l:192, t:72, w:38, h:22}
Description: none [false]
Kbshortcut: none [false]
DefAction: "Open"
Parent: none [false]:tool bar
Help: none [false]
Help Topic: none [false]
ChildCount: 1
Window: 0x0006038A class="ToolbarWindow32" style=0x5601A945 ex=0x100000
Children: "Open" : drop down button : hot tracked
Selection: none [empty]
Ancestors: none [false] : tool bar : normal
none [false] : window : normal
none [false] : client : default,focusable
none [false] : window : normal
none [false] : client : normal
none [false] : window : normal
none [false] : client : normal
none [false] : window : normal
"Flex Development - file:/C:/Program Files/Adobe/Flex Builder 3/plugins/com.adobe.flexbuilder.ui_3.0.194161/welcome/welcome.html - Adobe Flex Builder 3" : client : normal
"Flex Development - file:/C:/Program Files/Adobe/Flex Builder 3/plugins/com.adobe.flexbuilder.ui_3.0.194161/welcome/welcome.html - Adobe Flex Builder 3" : window : sizeable,moveable,focusable
"Desktop" : client : normal
"Desktop" : window : normal
[ No Parent ]
The Inspect32 tool reveals that the pop-up button should have the role of "split button" and one child named "Open" with the role "drop down button." When the control has been moused over or has keyboard focus, its state should be "hot tracked." A search on http://msdn.microsoft.com/ for "SplitButton accessibility" returns a more detailed and useful description of the SplitButton control's accessibiliy implementation, Working with Active Accessibility in the 2007 Office Fluent User Interface: The SplitButton Control. This more detailed description states that we should also expose the state of the child drop-down button that opens a popup menu as "has popup."
Update the PopUpButtonAccImpl constructor function at line 199 to read:
…
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Creates a new PopUpButtonAccImpl instance for the specified PopUpButton component.
*
* <p>Direct calls to the AccImpl subclass constructors are unneccessary.
* When a Flex project is compiled with Generate accessible SWF file set to <code>true</code>,
* the compiler instantiates the accessibility implementations for the components used in the
* project that have them by calling the the static <code>enableAccessibility()</code> method.</p>
*
* @param master The UIComponent instance that this PopUpButtonAccImpl instance is making accessible.
*
* @see ../../../html/help.html?content=accessible_4.html Configuring Flex applications for accessibility
*/
public function PopUpButtonAccImpl(master:UIComponent)
{
super(master);
role = ROLE_SYSTEM_SPLITBUTTON;
}
…
The constructor function defines the role property for the PopUpButtonAccImpl as ROLE_SYSTEM_SPLITBUTTON (0x3e).
Update the eventsToHandle() method at line 237 to include any additional events to which the PopUpButtonAccImpl should listen from its master component. In this example, the events should be the same as for the ButtonAccImpl, "click" and "labelChanged".
…
//----------------------------------
// eventsToHandle
//----------------------------------
/**
* @inheritDoc
*/
override protected function get eventsToHandle():Array
{
return super.eventsToHandle.concat([ "click", "labelChanged" ]);
}
…
Update the get_accRole() method at line 247 to read:
…
/**
* IAccessible method for returning system role for the component.
* System roles are predefined for all the components in MSAA.
* <p>A PopUpButton component, ( <code>childID == 0</code> ), reports the role <code>ROLE_SYSTEM_SPLITBUTTON</code> (0x3E).
* The PopUpButton's drop down arrow ( <code>childID == 1</code> ) reports the role <code>ROLE_SYSTEM_BUTTONDROPDOWN</code> (0x3B).</p>
*
* @param childID An unsigned integer corresponding to one of the component's child elements as defined by
* <code><a href="#getChildIDArray()">getChildIDArray()</a></code>.
*
* @return Role associated with the component.
*
* @tiptext Returns the system role for the component
* @helpid 3009
*
* @see ../../flash/accessibility/constants.html#roles AccessibilityImplementation Constants: Object Roles
* @see http://msdn.microsoft.com/en-us/library/ms696113(VS.85).aspx Microsoft Accessibility Developer Center: IAccessible::get_accRole
*/
override public function get_accRole(childID:uint):uint
{
if (childID == 0)
return role;
return ROLE_SYSTEM_BUTTONDROPDOWN;
}
…
The get_accRole() method should return the appropriate role constant for the master component or for one of its children. The PopUpButtonAccImpl returns ROLE_SYSTEM_SPLITBUTTON (0x3e), while its child drop-down button returns ROLE_SYSTEM_BUTTONDROPDOWN (0x38).
Add the following get_accValue() method override at line 272:
…
/**
* IAccessible method for returning the value of the PopUpButton
* which is spoken out by the screen reader.
* The PopUpButton should return value that would be applied when clicked. For example, the value of the PopUpButton's label property.
*
* @param childID An unsigned integer corresponding to one of the component's child elements as defined by
* <code><a href="#getChildIDArray()">getChildIDArray()</a></code>.
*
* @return Value String
*
* @see http://msdn.microsoft.com/en-us/library/ms697312(VS.85).aspx Microsoft Accessibility Developer Center: IAccessible::get_accValue
* @see http://msdn.microsoft.com/en-us/library/bb404170.aspx#ActiveAccessibility2007OfficeFluentUI_TheSplitButtonControl Working with Active Accessibility in the 2007 Office Fluent User Interface: The SplitButton Control
*/
override public function get_accValue(childID:uint):String
{
var accValue:String;
// local reference to the master component as a PopUpButton
var popUpButton:PopUpButton = PopUpButton(master);
if(childID == 0){
// the PopUpButton component itself
// local reference to the label
var label:String = popUpButton.label;
if(popUpButton.popUp
&& popUpButton.popUp is Menu
&& label != null
&& label != ""){
// If the popUp exists and is a Menu and
// the label exists and is not an empty string...
var popUpMenu:Menu = popUpButton.popUp as Menu;
if(popUpMenu.itemToLabel(popUpMenu.selectedItem) == label){
// If the label matches the popUp menu's selectedIndex, return the label as the accValue.
accValue = label;
if(popUpButton.accessibilityProperties && popUpButton.accessibilityProperties.shortcut){
// If a keyboard shortcut is defined in the
// PopUpButton's .accessibilityProperties object,
// append it to the returned accValue.
accValue += " ("+popUpButton.accessibilityProperties.shortcut+")";
}
}
}
}
return accValue;
}
…
The get_accValue() method returns the appropriate value for the component or for one of its child elements. If the PopUpButton component's label property is equal to the label of the selectedItem in its popUp menu, the get_accValue() method should return the value of the label property.
Update the get_accState() method at line 321 to read:
…
/**
* IAccessible method for returning the state of the PopUpButton.
* States are predefined for all the components in MSAA.
* Values are assigned to each state.
* Depending upon the PopUpButton being pressed or released, its popUp being present and opened or closed, and the PopUpButton being hovered over or focused,
* a value is returned.
*
* @param childID An unsigned integer corresponding to one of the component's child elements as defined by
* <code><a href="#getChildIDArray()">getChildIDArray()</a></code>.
*
* @return State indicating whether the PopUpButton is pressed or released.
*
* @see #getChildIDArray()
* @see flash.accessibility.AccessibilityImplementation#get_accState()
* @see ../../flash/accessibility/constants.html#states AccessibilityImplementation Constants: Object State Constants
* @see http://msdn.microsoft.com/en-us/library/ms696191(VS.85).aspx Microsoft Accessibility Developer Center: IAccessible::get_accState
*/
override public function get_accState(childID:uint):uint
{
// the normal default state
var accState:uint = 0;
// local reference to the master component as a PopUpButton
var popUpButton:PopUpButton = PopUpButton(master);
if(childID == 1){
// the drop-down button
if(popUpButton.popUp){
// if the PopUpButton's popUp property is defined,
// indicate that the drop-down button has a pop up.
accState = STATE_SYSTEM_HASPOPUP;
}
if(popUpButton.mx_internal::isShowingPopUp == true){
// if the popUp is showing,
// indicate that the drop-down button is pressed
accState |= STATE_SYSTEM_PRESSED;
}
} else {
// the PopUpButton component itself
// the component state inherited from AccImpl
// unavailable, normal, focusable, or focused
accState = getState(childID);
if (popUpButton.selected) {
// if the popUpButton is selected,
// indicate that popUpButton is pressed
accState |= STATE_SYSTEM_PRESSED;
}
}
var mouseX:Number = master.mouseX;
var mouseY:Number = master.mouseY;
var bounds:Rectangle = master.getBounds(master);
if((mouseX >= bounds.x
&& mouseX <= (bounds.x + bounds.width)
&& mouseY >= bounds.y
&& mouseY <= (bounds.y + bounds.height))
|| (popUpButton.focusManager.getFocus() == popUpButton)){
// if the parent popUpButton component or child drop-down button
// has either mouse or keyboard focus,
// indicate that it is hot-tracked.
accState |= STATE_SYSTEM_HOTTRACKED;
}
return accState;
}
…
The get_accState() method should return the appropriate state constant or state constants for the master component or for one of its children. If the PopUpButton has a popUp property, its child drop-down button should have the state STATE_SYSTEM_HASPOPUP (0x40000000). If the popUp is showing, the drop-down button should also have the state STATE_SYSTEM_PRESSED (0x00000008). If the PopUpButton itself is selected, it should have the state STATE_SYSTEM_PRESSED (0x00000008). Both the PopUpButton and its child drop-down button should have the state STATE_SYSTEM_HOTTRACKED (0x00000080) when the PopUpButton is either moused over or has keyboard focus.
Add the following getChildIDArray() method override at line 398:
…
/**
* Method to return an array of childIDs.
*
* @return An array of unsigned integer IDs with a length of one for the drop-down menu button.
*/
override public function getChildIDArray():Array
{
var childIDs:Array = [1];
return childIDs;
}
…
The getChildIDArray() method returns an array containing the unsigned integer IDs of all child elements in the accessibility implementation. The length of the array may be zero. The IDs in the array should appear in the same logical order as the child elements they represent. This method is mandatory for any accessibility implementation that can contain child elements; otherwise it should not be implemented. For the PopUpButtonAccImpl, the getChildIDArray() method returns an array containing a single element with a value of 1, which represents the PopUpButton's child drop-down button element.
Update the get_accDefaultAction() method at line 410 to return the appropriate default action for the master component or for one of its children. For the PopUpButton component itself, the method returns “Press”, and for the component's child drop-down button, the method returns “Open.”
…
/**
* IAccessible method for returning the default action of the PopUpButton, which is Press for the component itself and Open for its child drop-down menu button.
*
* @param childID An unsigned integer corresponding to one of the component's child elements as defined by
* <code><a href="#getChildIDArray()">getChildIDArray()</a></code>.
*
* @return DefaultAction String
*
* @see http://msdn.microsoft.com/en-us/library/ms696144(VS.85).aspx Microsoft Accessibility Developer Center: IAccessible::get_accDefaultAction
*/
override public function get_accDefaultAction(childID:uint):String
{
if(childID == 0){
return "Press";
}
return "Open";
}
…
Update the addDoDefaultAction() method at line 439 to read:
…
/**
* IAccessible method for performing the default action of the PopUpButton, which is Press for the component itself and Open for its child drop-down menu button.
*
* @param childID An unsigned integer corresponding to one of the component's child elements as defined by
* <code><a href="#getChildIDArray()">getChildIDArray()</a></code>.
*
* @see http://msdn.microsoft.com/en-us/library/ms696119(VS.85).aspx Microsoft Accessibility Developer Center: IAccessible::accDoDefaultAction
*/
override public function accDoDefaultAction(childID:uint):void
{
var popUpButton:PopUpButton = master as PopUpButton;
if (childID==0)
{
var event:KeyboardEvent = new KeyboardEvent(KeyboardEvent.KEY_DOWN);
event.keyCode = Keyboard.SPACE;
master.dispatchEvent(event);
event = new KeyboardEvent(KeyboardEvent.KEY_UP);
event.keyCode = Keyboard.SPACE;
master.dispatchEvent(event);
} else if(childID == 1){
if(popUpButton.mx_internal::isShowingPopUp == true){
popUpButton.close();
} else {
popUpButton.open();
}
}
}
…
The addDoDefaultAction() method executes the default action for the master component or for one of its children. For the PopUpButton component itself, the method simulates a button click. For the child drop-down button element, the method either opens or closes the popUp.
Update the getName() method at line 463 to read:
…
/**
* Method for returning the name of the PopUpButton which is spoken out by the screen reader.
* The PopUpButton should return the label inside as the name of the PopUpButton.
* The child drop-down button should retrurn "Open" or "Close".
*
* @param childID An unsigned integer corresponding to one of the component's child elements as defined by
* <code><a href="#getChildIDArray()">getChildIDArray()</a></code>.
*
* @return Name string corresponding to the PopUpButton label. "Open" or "Close" for the child drop-down button.
*/
override protected function getName(childID:uint):String
{
var popUpButton:PopUpButton = master as PopUpButton;
var popUp:UIComponent = popUpButton.popUp as UIComponent;
if(popUp && popUp is Menu){
// a tweak to add Context as the name of the popUp menu if no other name is defined
if(!popUp.accessibilityProperties){
popUp.accessibilityProperties = new AccessibilityProperties();
}
if(popUp is Menu && popUp.accessibilityProperties.name == ""){
popUp.accessibilityProperties.name = "Context";
}
}
if(childID == 1){
// For the drop-down button, if the popUp is showing, return Close, otherwise return Open.
return (popUpButton.mx_internal::isShowingPopUp == true) ? "Close" : "Open";
}
// For the popUpButton component itself, return the label property.
var label:String = popUpButton.label;
return label != null && label != "" ? label : "";
}
…
The getName() method returns the appropriate name for the master component or for one of its children. For the PopUpButton component itself, the method should return its label property as its name. The component's child drop-down button returns either “Open” or “Close” depending on whether or not the popUp is showing. For the PopUpButton, this method also adds “Context” as the name of the popUp menu if no other name has been defined.
Add the following accLocation() method at line 498:
…
/**
* IAccessible method for returning the bounding box of a the PopUpButton or its child drop-down arrow.
*
* @param childID An unsigned integer corresponding to one of the component's child elements as defined by
* <code><a href="#getChildIDArray()">getChildIDArray()</a></code>.
*
* @return The PopUpButton or its drop-down button.
*
* @see http://msdn.microsoft.com/en-us/library/ms696118(VS.85).aspx Microsoft Accessibility Developer Center: IAccessible::accLocation
*/
override public function accLocation(childID:uint):*
{
// the master component
var location:* = master;
if(childID == 1){
// calculate the rectangle location of the child drop-down button
var popUpButton:PopUpButton = master as PopUpButton;
var popUpButtonRect:Rectangle = popUpButton.getRect(popUpButton);
var arrowButtonsWidth:Number = popUpButton.mx_internal::getArrowButtonsWidth();
location = new Rectangle(
popUpButtonRect.x + popUpButton.width - arrowButtonsWidth,
popUpButtonRect.y,
arrowButtonsWidth,
popUpButton.height
);
}
return location;
}
…
The accLocation() method returns the bounding box of the master component, or that of one of its children, relative to the master component's stage. The PopUpButton returns itself as the its bounding box location. Its child drop-down button returns its bounding box as a Rectangle with its bounds determined programmatically.
Update the eventHandler() method at line 535 to handle any events to which the PopUpButtonAccImpl should listen from its master component. In this example, the handled events should be the same as for the ButtonAccImpl, "click" and "labelChanged".
…
/**
* Override the generic event handler.
* Each AccImpl subclass must implement this method to listen for events from its master component.
*
* @param event The event object.
*
* @see mx.accessibility.AccImpl#eventHandler()
*/
override protected function eventHandler(event:Event):void
{
switch (event.type)
{
case "click":
{
Accessibility.sendEvent(master, 0, EVENT_OBJECT_STATECHANGE);
Accessibility.updateProperties();
break;
}
case "labelChanged":
{
Accessibility.sendEvent(master, 0, EVENT_OBJECT_NAMECHANGE);
Accessibility.updateProperties();
break;
}
}
}
…
The eventHandler() method handles events from the master component and sends the appropriate object or system event to MSAA using the flash.accessibility.Accessibility.sendEvent() method. Depending on the object or system event it receives, the MSAA system will call the IAccessible method to retrieve the appropriate value from the object that sent the event.
Once you've added the accessibility mix-in methods to your component and have created the AccImpl subclass, you are ready to test the accessibility implementation.
Copy the following MXML code sample into the default MXML application for your PopUpMenuButton Flex project. The application is a slightly modified version of the PopUpMenuButton example described in the article PopUpMenuButton control. The modification adds event handlers to retain the selectedItem in the popUp menu when the menu is opened and closed, which will allow us to test the PopUpButtonAccImpl.get_accValue() method.
<?xml version="1.0"?>
<!-- menus/PopUpMenuButtonControl.mxml -->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" viewSourceURL="srcview/index.html">
<mx:Script>
<![CDATA[
import mx.events.DropdownEvent;
import mx.events.MenuEvent;
import mx.controls.Menu;
private var selectedIndex:int = 2;
// The initData function sets the initial value of the button
// label by setting the Menu subcontrol's selectedIndex property.
// You must cast the popUp property to a Menu.
private function initData():void {
var menu:Menu = Menu(pb2.popUp);
menu.selectedIndex = selectedIndex;
pb2.addEventListener(MenuEvent.ITEM_CLICK, itemClickHandler);
pb2.addEventListener(DropdownEvent.OPEN, openHandler);
pb2.addEventListener(DropdownEvent.CLOSE, closeHandler);
}
private function itemClickHandler(event:MenuEvent):void {
var menu:Menu = Menu(event.currentTarget.popUp);
selectedIndex=menu.selectedIndex;
}
private function openHandler(event:DropdownEvent):void {
var menu:Menu = Menu(pb2.popUp);
menu.selectedIndex = selectedIndex;
}
private function closeHandler(event:DropdownEvent):void {
var menu:Menu = Menu(pb2.popUp);
selectedIndex = (selectedIndex != menu.selectedIndex) ? menu.selectedIndex : selectedIndex;
menu.selectedIndex = selectedIndex;
}
]]>
</mx:Script>
<mx:XML format="e4x" id="dp2">
<root>
<editItem label="Cut"/>
<editItem label="Copy"/>
<editItem label="Paste"/>
<separator type="separator"/>
<editItem label="Delete"/>
</root>
</mx:XML>
<mx:PopUpMenuButton id="pb2"
dataProvider="{dp2}"
labelField="@label"
showRoot="false"
creationComplete="initData();" />
</mx:Application>
Compile and debug the application in either Internet Explorer or Firefox. Open the MSAA SDK Tool Inspect32. Focus on the Flash movie in the browser window and navigate to focus on the PopUpMenu instance in your application using the the keyboard or the mouse. Inspect32 should track your focus and update its display window with information similar to the following:
How found: Focus [o:0xFFFFFFFC,c:0x1]
hwnd=0x000C0976 32bit class="MacromediaFlashPlayerActiveX" style=0x56000000 ex=0x0
Info: IAcc = 0x001F1568 VarChild:[VT_I4=0x0]
Interfaces: IEnumVARIANT
Impl: Remote native IAccessible
Annotation ID: [not supported]
Name: "Paste"
Value: "Paste"
Role: split button
State: focused,hot tracked,focusable
Location: {l:802, t:147, w:63, h:22}
Description: none [mnfd]
Kbshortcut: none [mnfd]
DefAction: "Press"
Parent: none [mnfd]:client
Help: none [mnfd]
Help Topic: none [mnfd]
ChildCount: 1
Window: 0x000C0976 class="MacromediaFlashPlayerActiveX" style=0x56000000 ex=0x0
Children: "Open" : drop down button : hot tracked,has popup
Selection: none [mnfd]
Ancestors: none [mnfd] : client : focusable
[ No Parent ]
Inspect32 reveals how the PopUpButton Accessibility Implementation exposes itself to MSAA, as an object with the role “split button,”, with name and value equal to “Paste,” having the default action “Press.” The object's state includes “focused” and “hot tracked”. It contains a single child with the role “drop down button,” that has the name “Open.” The child object's state includes “hot tracked” and “has popup.”
Inspect32 also allows you to test navigation and interaction programmatically. For example, with the Flash movie focused, type Ctrl+Shift+F12 to navigate to the next item or type Ctrl+Shift+F11 to navigate to the previous item in the MSAA hierarchy. Use Ctrl+Shift+F12 and/or Ctrl+Shift+F11 to set focus on the PopUpMenuButton instance's child drop down button. Type Ctrl+Shift+F2 to execute the default action for the drop down button. This should open the popUp menu. The drop down button name should change to “Close,” and its state should indicate “pressed.” Type Ctrl+Shift+F2 again to close the menu.
Once you're fairly confident that your accessibility implementation is returning appropriate values for its IAccessible methods, you should test again using a screen reader that is capable of interpreting Flash content such as Freedom Scientific's JAWS or GW Micro's Window-Eyes.
It is not uncommon for a component to not behave quite as expected when testing it with a screen reader. For example, the keyboard command Ctrl+Down Arrow, which is used in the PopUpButton component to open the popUp menu, is also the standard keyboard command in JAWS to move one paragraph down in the text. Using Ctrl+Shift+Down Arrow instead of Ctrl+Down Arrow will open the menu, but it's not obvious to the user. A better solution is to update the PopUpButton component, to allow it to open by simply keying the Down Arrow.
Update the PopUpButton.keyDownHandler() method at line 907 in the PopUpButton class file for the PopUpMenuButton Flex project to read:
…
override protected function keyDownHandler(event:KeyboardEvent):void
{
super.keyDownHandler(event);
// listen for both Ctrl+Down Arrow and Down Arrow events
// if the popUp menu is not already showing
if ((event.ctrlKey && event.keyCode == Keyboard.DOWN)
|| (event.keyCode == Keyboard.DOWN &&!showingPopUp))
{
openWithEvent(event);
event.stopPropagation();
}
else if ((event.ctrlKey && event.keyCode == Keyboard.UP) ||
(event.keyCode == Keyboard.ESCAPE))
{
closeWithEvent(event);
event.stopPropagation();
}
else if (event.keyCode == Keyboard.ENTER &∓ showingPopUp)
{
// Redispatch the event to the popup
// and let its keyDownHandler() handle it.
_popUp.dispatchEvent(event);
closeWithEvent(event);
event.stopPropagation();
}
else if (showingPopUp &&
(event.keyCode == Keyboard.UP ||
event.keyCode == Keyboard.DOWN ||
event.keyCode == Keyboard.LEFT ||
event.keyCode == Keyboard.RIGHT ||
event.keyCode == Keyboard.PAGE_UP ||
event.keyCode == Keyboard.PAGE_DOWN))
{
// Redispatch the event to the popup
// and let its keyDownHandler() handle it.
_popUp.dispatchEvent(event);
event.stopPropagation();
}
}
…
Another peculiarity of JAWS is that it announces the role of the PopUpMenuButton component as a “button” as opposed to a “split button.” Even if you are providing a correct accessibility implementation to MSAA, if it is of a system role that does not have a precedent in Flex or Flash, like the PopUpButton, the screen reader may not have been scripted to understand how to interperet an object with that system role in Flash. The short term solution is to add the system role to the name returned by the PopUpButtonAccImpl.getName() method.
Update the getName() method at line 463 in the PopUpButtonAccImpl class definition to read:
…
/**
* Method for returning the name of the PopUpButton which is spoken out by the screen reader.
* The PopUpButton should return the label inside as the name of the PopUpButton.
* The child drop-down button should retrurn "Open" or "Close".
*
* @param childID An unsigned integer corresponding to one of the component's child elements as defined by
* <code><a href="#getChildIDArray()">getChildIDArray()</a></code>.
*
* @return Name string corresponding to the PopUpButton label. "Open" or "Close" for the child drop-down button.
*/
override protected function getName(childID:uint):String
{
var popUpButton:PopUpButton = master as PopUpButton;
var popUp:UIComponent = popUpButton.popUp as UIComponent;
if(popUp && popUp is Menu){
// a tweak to add Context as the name of the popUp menu if no other name is defined
if(!popUp.accessibilityProperties){
popUp.accessibilityProperties = new AccessibilityProperties();
}
if(popUp is Menu && popUp.accessibilityProperties.name == ""){
popUp.accessibilityProperties.name = "Context";
}
}
if(childID == 1){
// For the drop-down button, if the popUp is showing, return Close, otherwise return Open.
return (popUpButton.mx_internal::isShowingPopUp == true) ? "Close" : "Open";
}
// For the popUpButton component itself, return the label property.
var label:String = popUpButton.label;
return label != null && label != "" ? label + " Split Button" : "Split Button";
}
…
Here is the final example of the PopUpButton component with an accessibility implementation with source code:
For more information on the flash.accessibility.AccessibilityImplementation class and the accessibility implementations for Flex components defined in the mx.accessibility package, see the Adobe Flex Language Reference.