Bug 467057 - xul menulist doesn't fire expand/collapse state change events, r=marcoz, aaronlev

This commit is contained in:
Alexander Surkov 2008-12-03 17:04:02 +08:00
parent f27dd1d882
commit ec5bc8b940
5 changed files with 284 additions and 36 deletions

View File

@ -649,24 +649,13 @@ nsresult nsRootAccessible::HandleEventWithTarget(nsIDOMEvent* aEvent,
return NS_OK;
}
if (eventType.EqualsLiteral("popuphiding")) {
// If accessible focus was on or inside popup that closes,
// then restore it to true current focus.
// This is the case when we've been getting DOMMenuItemActive events
// inside of a combo box that closes. The real focus is on the combo box.
// It's also the case when a popup gets focus in ATK -- when it closes
// we need to fire an event to restore focus to where it was
if (!gLastFocusedNode ||
!nsCoreUtils::IsAncestorOf(aTargetNode, gLastFocusedNode)) {
return NS_OK; // And was not focused on an item inside the popup
}
// Focus was on or inside of a popup that's being hidden
FireCurrentFocusEvent();
}
nsCOMPtr<nsIAccessible> accessible;
accService->GetAccessibleInShell(aTargetNode, eventShell,
getter_AddRefs(accessible));
if (eventType.EqualsLiteral("popuphiding"))
return HandlePopupHidingEvent(aTargetNode, accessible);
nsCOMPtr<nsPIAccessible> privAcc(do_QueryInterface(accessible));
if (!privAcc)
return NS_OK;
@ -831,22 +820,7 @@ nsresult nsRootAccessible::HandleEventWithTarget(nsIDOMEvent* aEvent,
nsAccUtils::FireAccEvent(nsIAccessibleEvent::EVENT_ALERT, accessible);
}
else if (eventType.EqualsLiteral("popupshown")) {
// Don't fire menupopup events for combobox and autocomplete lists
PRUint32 role = nsAccUtils::Role(accessible);
PRInt32 event = 0;
if (role == nsIAccessibleRole::ROLE_MENUPOPUP) {
event = nsIAccessibleEvent::EVENT_MENUPOPUP_START;
}
else if (role == nsIAccessibleRole::ROLE_TOOLTIP) {
// There is a single <xul:tooltip> node which Mozilla moves around.
// The accessible for it stays the same no matter where it moves.
// AT's expect to get an EVENT_SHOW for the tooltip.
// In event callback the tooltip's accessible will be ready.
event = nsIAccessibleEvent::EVENT_ASYNCH_SHOW;
}
if (event) {
nsAccUtils::FireAccEvent(event, accessible);
}
HandlePopupShownEvent(accessible);
}
else if (eventType.EqualsLiteral("DOMMenuInactive")) {
if (nsAccUtils::Role(accessible) == nsIAccessibleRole::ROLE_MENUPOPUP) {
@ -1103,6 +1077,94 @@ NS_IMETHODIMP nsRootAccessible::FireDocLoadEvents(PRUint32 aEventType)
return NS_OK;
}
nsresult
nsRootAccessible::HandlePopupShownEvent(nsIAccessible *aAccessible)
{
PRUint32 role = nsAccUtils::Role(aAccessible);
if (role == nsIAccessibleRole::ROLE_MENUPOPUP) {
// Don't fire menupopup events for combobox and autocomplete lists.
return nsAccUtils::FireAccEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_START,
aAccessible);
}
if (role == nsIAccessibleRole::ROLE_TOOLTIP) {
// There is a single <xul:tooltip> node which Mozilla moves around.
// The accessible for it stays the same no matter where it moves.
// AT's expect to get an EVENT_SHOW for the tooltip.
// In event callback the tooltip's accessible will be ready.
return nsAccUtils::FireAccEvent(nsIAccessibleEvent::EVENT_ASYNCH_SHOW,
aAccessible);
}
if (role == nsIAccessibleRole::ROLE_COMBOBOX_LIST) {
// Fire expanded state change event for comboboxes and autocompeletes.
nsCOMPtr<nsIAccessible> comboboxAcc;
nsresult rv = aAccessible->GetParent(getter_AddRefs(comboboxAcc));
NS_ENSURE_SUCCESS(rv, rv);
PRUint32 comboboxRole = nsAccUtils::Role(comboboxAcc);
if (comboboxRole == nsIAccessibleRole::ROLE_COMBOBOX ||
comboboxRole == nsIAccessibleRole::ROLE_AUTOCOMPLETE) {
nsCOMPtr<nsIAccessibleStateChangeEvent> event =
new nsAccStateChangeEvent(comboboxAcc,
nsIAccessibleStates::STATE_EXPANDED,
PR_FALSE, PR_TRUE);
NS_ENSURE_TRUE(event, NS_ERROR_OUT_OF_MEMORY);
nsCOMPtr<nsPIAccessible> pComboboxAcc(do_QueryInterface(comboboxAcc));
return pComboboxAcc->FireAccessibleEvent(event);
}
}
return NS_OK;
}
nsresult
nsRootAccessible::HandlePopupHidingEvent(nsIDOMNode *aNode,
nsIAccessible *aAccessible)
{
// If accessible focus was on or inside popup that closes, then restore it
// to true current focus. This is the case when we've been getting
// DOMMenuItemActive events inside of a combo box that closes. The real focus
// is on the combo box. It's also the case when a popup gets focus in ATK --
// when it closes we need to fire an event to restore focus to where it was.
if (gLastFocusedNode &&
nsCoreUtils::IsAncestorOf(aNode, gLastFocusedNode)) {
// Focus was on or inside of a popup that's being hidden
FireCurrentFocusEvent();
}
// Fire expanded state change event for comboboxes and autocompletes.
if (!aAccessible)
return NS_OK;
PRUint32 role = nsAccUtils::Role(aAccessible);
if (role != nsIAccessibleRole::ROLE_COMBOBOX_LIST)
return NS_OK;
nsCOMPtr<nsIAccessible> comboboxAcc;
nsresult rv = aAccessible->GetParent(getter_AddRefs(comboboxAcc));
NS_ENSURE_SUCCESS(rv, rv);
PRUint32 comboboxRole = nsAccUtils::Role(comboboxAcc);
if (comboboxRole == nsIAccessibleRole::ROLE_COMBOBOX ||
comboboxRole == nsIAccessibleRole::ROLE_AUTOCOMPLETE) {
nsCOMPtr<nsIAccessibleStateChangeEvent> event =
new nsAccStateChangeEvent(comboboxAcc,
nsIAccessibleStates::STATE_EXPANDED,
PR_FALSE, PR_FALSE);
NS_ENSURE_TRUE(event, NS_ERROR_OUT_OF_MEMORY);
nsCOMPtr<nsPIAccessible> pComboboxAcc(do_QueryInterface(comboboxAcc));
return pComboboxAcc->FireAccessibleEvent(event);
}
return NS_OK;
}
#ifdef MOZ_XUL
nsresult
nsRootAccessible::HandleTreeRowCountChangedEvent(nsIDOMEvent *aEvent,

View File

@ -133,15 +133,15 @@ class nsRootAccessible : public nsDocAccessibleWrap,
void GetChromeEventHandler(nsIDOMEventTarget **aChromeTarget);
/**
* Handles 'TreeRowCountChanged' event. Used in HandleEventWithTarget().
* Used in HandleEventWithTarget().
*/
nsresult HandlePopupShownEvent(nsIAccessible *aAccessible);
nsresult HandlePopupHidingEvent(nsIDOMNode *aNode,
nsIAccessible *aAccessible);
#ifdef MOZ_XUL
nsresult HandleTreeRowCountChangedEvent(nsIDOMEvent *aEvent,
nsIAccessibleTreeCache *aAccessible);
/**
* Handles 'TreeInvalidated' event. Used in HandleEventWithTarget().
*/
nsresult HandleTreeInvalidatedEvent(nsIDOMEvent *aEvent,
nsIAccessibleTreeCache *aAccessible);

View File

@ -69,6 +69,7 @@ _TEST_FILES =\
test_nsIAccessible_actions.html \
$(warning test_nsIAccessible_actions.xul temporarily disabled) \
test_nsIAccessible_applicationAccessible.html \
test_nsIAccessible_comboboxes.xul \
test_nsIAccessible_editablebody.html \
test_nsIAccessible_editabledoc.html \
test_nsIAccessible_name.html \

View File

@ -4,6 +4,9 @@
const nsIAccessibleRetrieval = Components.interfaces.nsIAccessibleRetrieval;
const nsIAccessibleEvent = Components.interfaces.nsIAccessibleEvent;
const nsIAccessibleStateChangeEvent =
Components.interfaces.nsIAccessibleStateChangeEvent;
const nsIAccessibleStates = Components.interfaces.nsIAccessibleStates;
const nsIAccessibleRole = Components.interfaces.nsIAccessibleRole;
const nsIAccessibleTypes = Components.interfaces.nsIAccessibleTypes;
@ -195,6 +198,71 @@ function unregisterA11yEventListener(aEventType, aEventHandler)
}
}
/**
* Creates event queue for the given event type. The queue consists of invoker
* objects, each of them generates the event of the event type. When queue is
* started then every invoker object is asked to generate event after timeout.
* When event is caught then current invoker object is asked to check wether
* event was handled correctly.
*
* Invoker interface is:
* var invoker = {
* invoke: function(){}, // generates event for the DOM node
* check: function(aEvent){}, // checks event for correctness
* DOMNode getter() {} // DOM node event is generated for
* };
*
* @param aEventType the given event type
*/
function eventQueue(aEventType)
{
/**
* Add invoker object into queue.
*/
this.push = function eventQueue_push(aEventInvoker)
{
this.mInvokers.push(aEventInvoker);
}
/**
* Start the queue processing.
*/
this.invoke = function eventQueue_invoke()
{
window.setTimeout(
function(aQueue)
{
if (aQueue.mIndex == aQueue.mInvokers.length - 1) {
unregisterA11yEventListener(aQueue.mEventType, aQueue.mEventHandler);
for (var idx = 0; idx < aQueue.mInvokers.length; idx++)
ok(aQueue.mInvokers[idx].wasCaught, "test " + idx + " failed.");
SimpleTest.finish();
return;
}
aQueue.mInvokers[++aQueue.mIndex].invoke();
aQueue.invoke();
},
100, this
);
}
this.getInvoker = function eventQueue_getInvoker()
{
return this.mInvokers[this.mIndex];
}
this.mEventType = aEventType;
this.mEventHandler = new eventHandlerForEventQueue(this);
registerA11yEventListener(this.mEventType, this.mEventHandler);
this.mInvokers = new Array();
this.mIndex = -1;
}
////////////////////////////////////////////////////////////////////////////////
// Private
@ -235,3 +303,18 @@ var gA11yEventObserver =
listenersArray[index].handleEvent(event);
}
};
function eventHandlerForEventQueue(aQueue)
{
this.handleEvent = function eventHandlerForEventQueue_handleEvent(aEvent)
{
var invoker = this.mQueue.getInvoker();
if (aEvent.DOMNode == invoker.DOMNode) {
invoker.check(aEvent);
invoker.wasCaught = true;
}
}
this.mQueue = aQueue;
}

View File

@ -0,0 +1,102 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
type="text/css"?>
<?xml-stylesheet href="chrome://browser/content/browser.css"
type="text/css"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="nsIAccessible interface for xul:menulist test.">
<script type="application/javascript"
src="chrome://mochikit/content/MochiKit/packed.js"></script>
<script type="application/javascript"
src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
<script type="application/javascript"
src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
<script type="application/javascript"
src="chrome://mochikit/content/a11y/accessible/common.js" />
<script type="application/javascript">
<![CDATA[
function openHideCombobox(aComboboxNode, aIsOpen)
{
this.invoke = function invoke()
{
synthesizeMouse(this.DOMNode, 5, 5, {});
}
this.check = function check(aEvent)
{
aEvent.QueryInterface(nsIAccessibleStateChangeEvent);
is(aEvent.state, nsIAccessibleStates.STATE_EXPANDED,
"Wrong state change event is handled.");
is(aEvent.isEnabled(), this.mIsOpen,
"Wrong value of state expanded.");
}
this.DOMNode = aComboboxNode;
this.mIsOpen = aIsOpen;
}
var gQueue = null;
function doTest()
{
gQueue = new eventQueue(nsIAccessibleEvent.EVENT_STATE_CHANGE);
var menulist = document.getElementById("menulist");
gQueue.push(new openHideCombobox(menulist, true));
gQueue.push(new openHideCombobox(menulist, false));
// XXX: searchbar doesn't fire state change events because accessible
// parent of combobox_list accessible is pushbutton accessible.
//var searchbar = document.getElementById("searchbar");
//gQueue.push(new openHideCombobox(searchbar, true));
//gQueue.push(new openHideCombobox(searchbar, false));
gQueue.invoke(); // Will call SimpleTest.finish();
}
// This is the hack needed for searchbar work outside of browser.
function getBrowser()
{
return {
mCurrentBrowser: { engines: new Array() }
};
}
SimpleTest.waitForExplicitFinish();
addLoadEvent(doTest);
]]>
</script>
<hbox style="overflow: auto;" flex="1">
<body xmlns="http://www.w3.org/1999/xhtml">
<a target="_blank"
href="https://bugzilla.mozilla.org/show_bug.cgi?id=467057"
title="xul menulist doesn't fire expand/collapse state change events">
Mozilla Bug 467057
</a>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<pre id="test">
</pre>
</body>
<vbox flex="1">
<menulist id="menulist">
<menupopup>
<menuitem label="item1"/>
<menuitem label="item2"/>
<menuitem label="item3"/>
</menupopup>
</menulist>
<searchbar id="searchbar"/>
</vbox>
</hbox>
</window>