From 25db64d5247756a8cc7893bce09ff9d85b7070fc Mon Sep 17 00:00:00 2001 From: Alexander Surkov Date: Sun, 28 Jul 2013 14:33:57 -0400 Subject: [PATCH] Bug 810268 - there's no way to know unselected item when selection in single selection was changed, r=tbsaunde --- accessible/src/base/EventQueue.cpp | 25 +++- accessible/src/base/nsEventShell.h | 14 ++ accessible/tests/mochitest/events.js | 126 ++++++++++++++---- .../mochitest/events/test_selection.html | 21 +-- .../mochitest/events/test_selection_aria.html | 2 +- 5 files changed, 154 insertions(+), 34 deletions(-) diff --git a/accessible/src/base/EventQueue.cpp b/accessible/src/base/EventQueue.cpp index b7f84ee0d01..c9235f2c8af 100644 --- a/accessible/src/base/EventQueue.cpp +++ b/accessible/src/base/EventQueue.cpp @@ -303,7 +303,7 @@ EventQueue::CoalesceSelChangeEvents(AccSelChangeEvent* aTailEvent, aTailEvent->mSelChangeType == AccSelChangeEvent::eSelectionRemove) { aTailEvent->mEventRule = AccEvent::eDoNotEmit; aThisEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION; - aThisEvent->mPackedEvent = aThisEvent; + aThisEvent->mPackedEvent = aTailEvent; return; } } @@ -472,6 +472,29 @@ EventQueue::ProcessEventQueue() continue; } + // Fire selected state change events in support to selection events. + if (event->mEventType == nsIAccessibleEvent::EVENT_SELECTION_ADD) { + nsEventShell::FireEvent(event->mAccessible, states::SELECTED, + true, event->mIsFromUserInput); + + } else if (event->mEventType == nsIAccessibleEvent::EVENT_SELECTION_REMOVE) { + nsEventShell::FireEvent(event->mAccessible, states::SELECTED, + false, event->mIsFromUserInput); + + } else if (event->mEventType == nsIAccessibleEvent::EVENT_SELECTION) { + AccSelChangeEvent* selChangeEvent = downcast_accEvent(event); + nsEventShell::FireEvent(event->mAccessible, states::SELECTED, + (selChangeEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd), + event->mIsFromUserInput); + + if (selChangeEvent->mPackedEvent) { + nsEventShell::FireEvent(selChangeEvent->mPackedEvent->mAccessible, + states::SELECTED, + (selChangeEvent->mPackedEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd), + selChangeEvent->mPackedEvent->mIsFromUserInput); + } + } + nsEventShell::FireEvent(event); // Fire text change events. diff --git a/accessible/src/base/nsEventShell.h b/accessible/src/base/nsEventShell.h index 9ccc0df5454..ee0dce21c92 100644 --- a/accessible/src/base/nsEventShell.h +++ b/accessible/src/base/nsEventShell.h @@ -35,6 +35,20 @@ public: mozilla::a11y::Accessible* aAccessible, mozilla::a11y::EIsFromUserInput aIsFromUserInput = mozilla::a11y::eAutoDetect); + /** + * Fire state change event. + */ + static void FireEvent(mozilla::a11y::Accessible* aTarget, uint64_t aState, + bool aIsEnabled, bool aIsFromUserInput) + { + nsRefPtr stateChangeEvent = + new mozilla::a11y::AccStateChangeEvent(aTarget, aState, aIsEnabled, + (aIsFromUserInput ? + mozilla::a11y::eFromUserInput : + mozilla::a11y::eNoUserInput)); + FireEvent(stateChangeEvent); + } + /** * Append 'event-from-input' object attribute if the accessible event has * been fired just now for the given node. diff --git a/accessible/tests/mochitest/events.js b/accessible/tests/mochitest/events.js index b2f5aac9d2e..4a429ae9331 100644 --- a/accessible/tests/mochitest/events.js +++ b/accessible/tests/mochitest/events.js @@ -508,7 +508,7 @@ function eventQueue(aEventType) } } - var matchedChecker = null; + var hasMatchedCheckers = false; for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { var eventSeq = this.mScenarios[scnIdx]; @@ -516,9 +516,9 @@ function eventQueue(aEventType) var nextChecker = this.getNextExpectedEvent(eventSeq); if (nextChecker) { if (eventQueue.compareEvents(nextChecker, aEvent)) { - matchedChecker = nextChecker; - matchedChecker.wasCaught++; - break; + this.processMatchedChecker(aEvent, nextChecker, scnIdx, eventSeq.idx); + hasMatchedCheckers = true; + continue; } } @@ -526,41 +526,46 @@ function eventQueue(aEventType) for (idx = 0; idx < eventSeq.length; idx++) { if (!eventSeq[idx].unexpected && eventSeq[idx].async) { if (eventQueue.compareEvents(eventSeq[idx], aEvent)) { - matchedChecker = eventSeq[idx]; - matchedChecker.wasCaught++; + this.processMatchedChecker(aEvent, eventSeq[idx], scnIdx, idx); + hasMatchedCheckers = true; break; } } } } - // Call 'check' functions on invoker's side. - if (matchedChecker) { - if ("check" in matchedChecker) - matchedChecker.check(aEvent); - + if (hasMatchedCheckers) { var invoker = this.getInvoker(); if ("check" in invoker) invoker.check(aEvent); } - // Dump handled event. - eventQueue.logEvent(aEvent, matchedChecker, this.areExpectedEventsLeft(), - this.mNextInvokerStatus); - // If we don't have more events to wait then schedule next invoker. - if (!this.areExpectedEventsLeft() && + if (this.hasMatchedScenario() && (this.mNextInvokerStatus == kInvokerNotScheduled)) { this.processNextInvokerInTimeout(); return; } // If we have scheduled a next invoker then cancel in case of match. - if ((this.mNextInvokerStatus == kInvokerPending) && matchedChecker) + if ((this.mNextInvokerStatus == kInvokerPending) && hasMatchedCheckers) this.mNextInvokerStatus = kInvokerCanceled; } // Helpers + this.processMatchedChecker = + function eventQueue_function(aEvent, aMatchedChecker, aScenarioIdx, aEventIdx) + { + aMatchedChecker.wasCaught++; + + if ("check" in aMatchedChecker) + aMatchedChecker.check(aEvent); + + eventQueue.logEvent(aEvent, aMatchedChecker, aScenarioIdx, aEventIdx, + this.areExpectedEventsLeft(), + this.mNextInvokerStatus); + } + this.getNextExpectedEvent = function eventQueue_getNextExpectedEvent(aEventSeq) { @@ -635,6 +640,16 @@ function eventQueue(aEventType) return false; } + + this.hasMatchedScenario = + function eventQueue_hasMatchedScenario() + { + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + if (!this.areExpectedEventsLeft(this.mScenarios[scnIdx])) + return true; + } + return false; + } this.getInvoker = function eventQueue_getInvoker() { @@ -858,6 +873,7 @@ eventQueue.isSameEvent = function eventQueue_isSameEvent(aChecker, aEvent) } eventQueue.logEvent = function eventQueue_logEvent(aOrigEvent, aMatchedChecker, + aScenarioIdx, aEventIdx, aAreExpectedEventsLeft, aInvokerStatus) { @@ -897,7 +913,8 @@ eventQueue.logEvent = function eventQueue_logEvent(aOrigEvent, aMatchedChecker, var currType = eventQueue.getEventTypeAsString(aMatchedChecker); var currTargetDescr = eventQueue.getEventTargetDescr(aMatchedChecker); - var consoleMsg = "*****\nEQ matched: " + currType + "\n*****"; + var consoleMsg = "*****\nScenario " + aScenarioIdx + + ", event " + aEventIdx + " matched: " + currType + "\n*****"; gLogger.logToConsole(consoleMsg); msg += " event, type: " + currType + ", target: " + currTargetDescr; @@ -1727,7 +1744,8 @@ function stateChangeChecker(aState, aIsExtraState, aIsEnabled, { if (aEvent instanceof nsIAccessibleStateChangeEvent) { var scEvent = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); - return aEvent.accessible = this.target && scEvent.state == aState; + return (aEvent.accessible == getAccessible(this.target)) && + (scEvent.state == aState); } return false; } @@ -1771,6 +1789,59 @@ function expandedStateChecker(aIsEnabled, aTargetOrFunc, aTargetFuncArg) } } +//////////////////////////////////////////////////////////////////////////////// +// Event sequances (array of predefined checkers) + +/** + * Event seq for single selection change. + */ +function selChangeSeq(aUnselectedID, aSelectedID) +{ + if (!aUnselectedID) { + return [ + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new invokerChecker(EVENT_SELECTION, aSelectedID) + ]; + } + + // Return two possible scenarios: depending on widget type when selection is + // moved the the order of items that get selected and unselected may vary. + return [ + [ + new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new invokerChecker(EVENT_SELECTION, aSelectedID) + ], + [ + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), + new invokerChecker(EVENT_SELECTION, aSelectedID) + ] + ]; +} + +/** + * Event seq for item removed form the selection. + */ +function selRemoveSeq(aUnselectedID) +{ + return [ + new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), + new invokerChecker(EVENT_SELECTION_REMOVE, aUnselectedID) + ]; +} + +/** + * Event seq for item added to the selection. + */ +function selAddSeq(aSelectedID) +{ + return [ + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new invokerChecker(EVENT_SELECTION_ADD, aSelectedID) + ]; +} + //////////////////////////////////////////////////////////////////////////////// // Private implementation details. //////////////////////////////////////////////////////////////////////////////// @@ -2043,16 +2114,23 @@ function sequenceItem(aProcessor, aEventType, aTarget, aItemID) /** * Invoker base class for prepare an action. */ -function synthAction(aNodeOrID, aCheckerOrEventSeq) +function synthAction(aNodeOrID, aEventsObj) { this.DOMNode = getNode(aNodeOrID); - if (aCheckerOrEventSeq) { - if (aCheckerOrEventSeq instanceof Array) { - this.eventSeq = aCheckerOrEventSeq; + if (aEventsObj) { + var scenarios = null; + if (aEventsObj instanceof Array) { + if (aEventsObj[0] instanceof Array) + scenarios = aEventsObj; // scenarios + else + scenarios = [ aEventsObj ]; // event sequance } else { - this.eventSeq = [ aCheckerOrEventSeq ]; + scenarios = [ [ aEventsObj ] ]; // a single checker object } + + for (var i = 0; i < scenarios.length; i++) + defineScenario(this, scenarios[i]); } this.getID = function synthAction_getID() diff --git a/accessible/tests/mochitest/events/test_selection.html b/accessible/tests/mochitest/events/test_selection.html index 7b2e32fc5fc..7581ff06ef8 100644 --- a/accessible/tests/mochitest/events/test_selection.html +++ b/accessible/tests/mochitest/events/test_selection.html @@ -38,31 +38,31 @@ gQueue.push(new synthClick("combobox", new invokerChecker(EVENT_FOCUS, "cb1_item1"))); gQueue.push(new synthDownKey("cb1_item1", - new invokerChecker(EVENT_SELECTION, "cb1_item2"))); + selChangeSeq("cb1_item1", "cb1_item2"))); // closed combobox gQueue.push(new synthEscapeKey("combobox", new invokerChecker(EVENT_FOCUS, "combobox"))); gQueue.push(new synthDownKey("cb1_item2", - new invokerChecker(EVENT_SELECTION, "cb1_item3"))); + selChangeSeq("cb1_item2", "cb1_item3"))); // listbox gQueue.push(new synthClick("lb1_item1", new invokerChecker(EVENT_SELECTION, "lb1_item1"))); gQueue.push(new synthDownKey("lb1_item1", - new invokerChecker(EVENT_SELECTION, "lb1_item2"))); + selChangeSeq("lb1_item1", "lb1_item2"))); // multiselectable listbox gQueue.push(new synthClick("lb2_item1", - new invokerChecker(EVENT_SELECTION, "lb2_item1"))); + selChangeSeq(null, "lb2_item1"))); gQueue.push(new synthDownKey("lb2_item1", - new invokerChecker(EVENT_SELECTION_ADD, "lb2_item2"), + selAddSeq("lb2_item2"), { shiftKey: true })); gQueue.push(new synthUpKey("lb2_item2", - new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item2"), + selRemoveSeq("lb2_item2"), { shiftKey: true })); gQueue.push(new synthKey("lb2_item1", " ", { ctrlKey: true }, - new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item1"))); + selRemoveSeq("lb2_item1"))); gQueue.invoke(); // Will call SimpleTest.finish(); } @@ -77,7 +77,12 @@ - Mozilla Bug 414302 + Bug 414302 + + + Bug 810268

diff --git a/accessible/tests/mochitest/events/test_selection_aria.html b/accessible/tests/mochitest/events/test_selection_aria.html index 0d21730b585..aabee46fd2e 100644 --- a/accessible/tests/mochitest/events/test_selection_aria.html +++ b/accessible/tests/mochitest/events/test_selection_aria.html @@ -36,7 +36,7 @@ this.invoke = function selectItem_invoke() { var itemNode = this.selectNode.querySelector("*[aria-selected='true']"); if (itemNode) - itemNode.removeAttribute("aria-selected", "true"); + itemNode.removeAttribute("aria-selected"); this.itemNode.setAttribute("aria-selected", "true"); }