No accessibility events for re-focused object after ARIA dialog dismissed

RESOLVED FIXED in Firefox 68

Status

()

defect
P1
normal
RESOLVED FIXED
4 years ago
2 months ago

People

(Reporter: jdiggs, Assigned: Jamie)

Tracking

(Blocks 3 bugs)

Trunk
mozilla68
Points:
---
Dependency tree / graph

Firefox Tracking Flags

(firefox42 wontfix, firefox68 fixed)

Details

Attachments

(4 attachments, 1 obsolete attachment)

Posted file test case: aria-modal-dialog.html (obsolete) —
Steps to reproduce:
1. Load the attached test case in Firefox
2. Launch DOM inspector and listen for (at least) focus events
3. Tab to the "Display a dialog" link
4. Sanity check: Press Ctrl+O to open native "Open" dialog. Press Escape.
5. Press Return on "Display a dialog" link. Press Escape.

Expected results: After both steps 4 and 5, there would be a focus event for the "Display a dialog" link which just re-gained focus after each dialog was dismissed.

Actual results: After step 4, there is a focus event for the "Display a dialog" link which just re-gained focus. However, after step 5, there is not a focus event for the "Display a dialog" link.

There are also missing window: accessibility events which I'll provide steps for in a subsequent comment.

Impact: Screen readers don't know where the user is after the ARIA dialog is dismissed.
Steps to reproduce:
1. Launch the attached accessible-event listener in terminal
2. Perform the dialog opening and dismissing steps from the opening report

Results:
* When the native dialog gains focus, the Firefox main window emits a window:deactivate event. (Good)
* When the native dialog is dismissed, the Firefox main window emits a window:activate event. (Good)
* When the ARIA dialog gains focus, it emits a window:activate event. (Good) BUT the Firefox main window does not emit a window:deactivate event. (Not good; but workaroundable)
* When the ARIA dialog loses focus, it emits a window:deactivate event. (Good) BUT the Firefox main window does not emit a window:activate event. (BAD and hard to reliably work around, especially given the lack of focus events mentioned in the opening report. Basically, all Orca knows is that the user is no longer in the ARIA dialog. Orca has no clue where the user actually is because nothing claims to be the active window or the focused object.)
Blocks: 1171559
Running the test case, I see that this apparently does not work as intended in the first place. In the Error Console, I see several of these errors:

attachment.cgi
Error
TypeError: okbutton is null
attachment.cgi
:39:3
Error
TypeError: pagebackground is null

So the intended logic doesn't kick in for me. The OK button doesn't gain focus when the dialog opens, and subsequently, the return to the other element doesn't work, either.
Ok, this version of the test case corrects the errors. The focus event for the link is present, but the window events I mentioned in my comment are still missing and that really should be fixed. The original, in-the-wild test case is not for a link-triggered popup dialog, but for a time-triggered one. (I'll see if I can make another test case in which the window events are missing and the focus events are also missing.)
Attachment #8643065 - Attachment is obsolete: true
Steps to reproduce:
1. Enable native caret navigation in Firefox
2. Load the test case
3. Tab to the "Line 1" link
4. Press Down arrow to move focus off that link to the text with "Line 2"
5. Wait 5 seconds for the dialog to appear
6. Press Escape

Expected results: At least one of the following (preferably both):
a. A window activate event for the Firefox window
b. A focus event for the document frame

Actual results: I don't see a or b. As a result, if the Orca user happens to not be on a focusable object at the time the dialog pops up, Orca gets no events indicating what has focus/is active after the dialog is dismissed.

In addition: The caret is no longer in the document frame, so the user cannot Down Arrow to Line 3.

This may be bad authoring, but it happens in the wild. If you (Firefox) happen to know the user is back in the document and the Firefox window is the active window, emitting the corresponding signals would be appreciated. Thanks!
Really simple test case:
data:text/html,<div role="dialog"><button autofocus onClick="this.parentNode.removeChild(this);">Close</button></div><button>Dummy</button>
Clicking the close button removes the dialog and fires no focus event. Happens all the time in the wild, and in many implementations, this can happen even if there *was* something focused before the dialog was opened. For NVDA, this is even worse because if the user cursors around, they'll still see the dialog content, since it's in a separate buffer.

Stated generally, the problem is that if focus is on an element which is removed (and blur isn't called before removal), no focus event is fired.

I think this is due to bug 559561. I thought there was a general a11y bug for this, but if there is, I can't find it.

It doesn't seem like bug 559561 is going anywhere and that'll probably require a lot of DOM/layout expertise to fix. I think we should just try to work around this in a11y for now.

Eitan, I seem to recall you saying you deal with this in AccessFu somehow. I'm guessing you watch for removals and adjust the virtual cursor? How do you manage this efficiently? Can we borrow some ideas more generally here?

Setting to p2 because this is super annoying for ARIA dialogs all over the place, but might need to adjust if a workaround turns out to be too complex. Marco, appreciate your thoughts on priority also. Have you seen this too?
Flags: needinfo?(mzehe)
Flags: needinfo?(eitan)
Priority: -- → P2
I always advise people to explicitly set focus to something after closing a dialog. Because browsers are in a habit of behaving inconsistently. So even if a button had focus and opened an ARIA dialog, which is only simulated anyway, the DOM doesn't remember where focus was before. Remember, ARIA dialogs aren't really dialogs for the browser. So, I tell people, if they make something close the dialog, make sure to set focus back to the control that opened it, or the control that makes most sense in the UX flow of the particular web application. And that generally works well. But as a web author, you have to consciously do that. And I think it's even documented like that in the authoring best practices.
Flags: needinfo?(mzehe)
(In reply to Marco Zehe (:MarcoZ) from comment #6)
> I always advise people to explicitly set focus to something after closing a
> dialog.

To clarify, I was more curious about what you see in the wild. I know what authors *should* do and I know the spec probably even *tells* them to do that. But in your experience, do real world sites actually follow that guidance? I've seen quite a lot in my time that don't, but maybe I'm an anomaly... and I can't think of specific examples right now, of course. :(

If our users are hitting this on a regular basis and we can alter behaviour to work around poor authoring without boiling the ocean, it makes sense to prioritise this. If, on the other hand, this kind of poor authoring is rare, this can e a p3 instead of a p2. (It's a p3 even aside from ARIA dialogs because focus should just never go to limbo at all.)
I think this is an implementation bug on our part. When a focused element is destroyed, the activeElement is reset to the <body> tag, just like if you did a blur() on the focused element. Except that in the former case we don't fire an event that the document was focused.

In accessfu we do weird dirty tricks where we listen for new dialogs and store the pre-dialog cursor position. When the dialog is dismissed we wait half a second to allow the web app to Do The Right Thing and explicitly set focus. If it doesn't we use the stored location, and hopefully it still exists.
Flags: needinfo?(eitan)
Of course, Joanie already knew this was a bug on our part 3 years ago :)
(In reply to Eitan Isaacson [:eeejay] from comment #8)
> I think this is an implementation bug on our part. When a focused element is
> destroyed, the activeElement is reset to the <body> tag, just like if you
> did a blur() on the focused element. Except that in the former case we don't
> fire an event that the document was focused.

I assumed this was because DOM isn't firing blur events for this case, which is apparently a DOM bug. See bug 559561. Are you saying there's more explicit a11y code for this case?

> In accessfu we do weird dirty tricks where we listen for new dialogs and
> store the pre-dialog cursor position. When the dialog is dismissed

How do you detect dialog dismissal?
(In reply to James Teh [:Jamie] from comment #10)
> (In reply to Eitan Isaacson [:eeejay] from comment #8)
> > I think this is an implementation bug on our part. When a focused element is
> > destroyed, the activeElement is reset to the <body> tag, just like if you
> > did a blur() on the focused element. Except that in the former case we don't
> > fire an event that the document was focused.
> 
> I assumed this was because DOM isn't firing blur events for this case, which
> is apparently a DOM bug. See bug 559561. Are you saying there's more
> explicit a11y code for this case?
> 

Yeah, not sure who is at fault. Looks like that bug got stuck because they want a destroyed element to send a blur event?
You had a similar bug open too: bug 1230420.

If this won't get resolved in DOM any time soon, I bet we can account for this in FocusManager.

> > In accessfu we do weird dirty tricks where we listen for new dialogs and
> > store the pre-dialog cursor position. When the dialog is dismissed
> 
> How do you detect dialog dismissal?

We listen for a hide event. If the vc position is defunct, or if it is in the subtree of the newly hidden accessible, we restore the pre-dialog position.

https://searchfox.org/mozilla-central/rev/97d488a17a848ce3bebbfc83dc916cf20b88451c/accessible/jsat/EventManager.jsm#338
I can fix this with a two line change, but it changes some assumptions we currently have in the code base.

In DocAccessible::UnbindFromDocument, we fire focus on the current focus if the active item was this accessible being unbound. However:
In FocusManager::ProcessFocusEvent, we currently only set mActiveItem if the DOM focus has a CurrentItem() (e.g. aria-activedescendant). When there's no CurrentItem, that means mActiveItem will be null.
Always setting mActiveItem to target, regardless of CurrentItem, fixes this bug.

The question is whether this is problematic. Tests pass and everything seems to work as expected.
However, this separation of active item and focus seems very deliberate; e.g. FocusManager::IsFocused vs FocusManager::IsActiveItem. Aside from being cautious about our focus not being updated correctly, is there any reason to keep this separation?
Flags: needinfo?(surkov.alexander)
Duplicate of this bug: 1230420
(In reply to James Teh [:Jamie] from comment #12)
> I can fix this with a two line change, but it changes some assumptions we
> currently have in the code base.
> 
> In DocAccessible::UnbindFromDocument, we fire focus on the current focus if
> the active item was this accessible being unbound. However:
> In FocusManager::ProcessFocusEvent, we currently only set mActiveItem if the
> DOM focus has a CurrentItem() (e.g. aria-activedescendant). When there's no
> CurrentItem, that means mActiveItem will be null.
> Always setting mActiveItem to target, regardless of CurrentItem, fixes this
> bug.

mActiveItem is about an active item within a widget, for example, a listitem of a listbox will be an active item, but listbox itself is not.

could you describe what happens here and why setting mActiveItem to a focused accessible will fix the bug.

> The question is whether this is problematic. Tests pass and everything seems
> to work as expected.
> However, this separation of active item and focus seems very deliberate;
> e.g. FocusManager::IsFocused vs FocusManager::IsActiveItem. Aside from being
> cautious about our focus not being updated correctly, is there any reason to
> keep this separation?

I think this is because FocusManager relies on DOM to track the focus, however in case of complex widgets like menus and listboxes where DOM focus doesn't match with the accessible focus, we use mActiveItem to track the accessible focus. So, yeah I think the main reason is to not to dupe focus info.
Flags: needinfo?(surkov.alexander)
(In reply to alexander :surkov (:asurkov) from comment #14)
> could you describe what happens here and why setting mActiveItem to a
> focused accessible will fix the bug.

When an accessible is unbound, we check if it is the currently active item. If it is, we fire a focus event for the accessible which now has the focus (e.g. the document). So, setting mActiveItem to focused accessibles allows this check to work for focused accessibles too, not just active items.

> I think this is because FocusManager relies on DOM to track the focus, ...
> I think the main reason is to not to dupe focus
> info.

The key problem is that DOM doesn't fire blur events when the focused node is removed. Ideally, this would be fixed in DOM, but that bug has been open for years and isn't getting any traction. Clearly, it isn't that important to anyone else, but it *is* important for a11y. So, I think we should just work around this in a11y.

If we don't want to mess with mActiveItem, one other way to do this would be to have an mLastFocused or similar which keeps track of the last accessible for which a focus event was fired. We can then check against that (instead of mActiveItem) when an accessible is unbound. Does this sound better to you?
Flags: needinfo?(surkov.alexander)
Sorry for delay. Still feel confusing on the bug.

I think I've been missing something here. So example from comment #5 has no active items involved, and it claims there's no focus on a document when dialog is closed. I don't get why. Is it because there's no blur event (bug 559561) and we don't process the focus properly when focused accessible shuts down?

Then, aria-descendants case (comment #14 scenario) looks different. FocusMgr()->ActiveItemChanged(nullptr) has to force us to fire an event for a DOM focused node if we have one. Thus I can think only of a problem if FocusedAccessible() returned null, but not sure this is the case here.

Jamie, can you help to connect dots?
Flags: needinfo?(surkov.alexander)
(In reply to alexander :surkov (:asurkov) from comment #16)
> I think I've been missing something here. So example from comment #5 has no
> active items involved, and it claims there's no focus on a document when
> dialog is closed. I don't get why. Is it because there's no blur event (bug
> 559561) and we don't process the focus properly when focused accessible
> shuts down?

Correct.

> Then, aria-descendants case (comment #14 scenario) looks different.

This bug is not about aria-activedescendant. That already works. My patch just makes it so that mActiveItem is used for focus as well as active items; i.e. we *always* keep track of the focus rather than relying on querying DOM.

If we did go with the approach in my patch, we should probably rename mActiveItem to mFocus or similar and get rid of the concept of separate focus/active item.

An alternative approach is to introduce an mLastFocus or similar and check that as well when an accessible is unbound. That does mean introducing a new variable, but means we still rely on DOM most of the time, only using mLastFocus when checking if the DOM focus died without firing a blur.

Is that any clearer?
(In reply to James Teh [:Jamie] from comment #17)

> > Then, aria-descendants case (comment #14 scenario) looks different.
> 
> This bug is not about aria-activedescendant. That already works. My patch
> just makes it so that mActiveItem is used for focus as well as active items;
> i.e. we *always* keep track of the focus rather than relying on querying DOM.

I think I understand the patch idea, what I miss is why we fail to fire focus event for a DOM focused element, when active descendant gets unbound from the tree.
Because there's no DOM blur, nor is there some other DOM event that would cause us to fire an event. In DocAccessible::UnbindFromDocument, we handle this for the active item:

1323      // Fire focus event on accessible having DOM focus if active item was removed
1324      // from the tree.
1325      if (FocusMgr()->IsActiveItem(aAccessible)) {
1326        FocusMgr()->ActiveItemChanged(nullptr);

The previous focus wasn't the "active item", so this code doesn't apply. My patch changes things so that it does. The alternative approach I mentioned would change:
FocusMgr()->IsActiveItem(aAccessible)
to something like:
FocusMgr()->WasLastFocus(aAccessible)
Even that name is a bit confusing because of the confusion between DOM focus and accessibility focus (where the latter might be DOM focus or active item). Maybe FocusMgr()->FocusWasLastFiredOn?
(In reply to James Teh [:Jamie] from comment #19)
> Because there's no DOM blur, nor is there some other DOM event that would
> cause us to fire an event. In DocAccessible::UnbindFromDocument, we handle
> this for the active item:
> 
> 1323      // Fire focus event on accessible having DOM focus if active item
> was removed
> 1324      // from the tree.
> 1325      if (FocusMgr()->IsActiveItem(aAccessible)) {
> 1326        FocusMgr()->ActiveItemChanged(nullptr);
> 
> The previous focus wasn't the "active item", so this code doesn't apply.

gotcha, storing mLastFocus should be an easy and performant way to fix it. However I think you may run into a rare bug, when a tree containing not yet fired focus gets shut down. But anyways you solve here the majority of the problem with mLastFocus thing, so you may leave this issue for a follow-up.
Assignee: nobody → jteh
Priority: P2 → P1

If the DOM focus is removed before something else is focused, the document gets DOM focus, but no blur event is fired.
This means that no a11y focus event is fired, so clients aren't notified.
This is particularly problematic for screen readers when dismissing some ARIA dialogs, as the screen reader doesn't know that focus has returned to the top level document.

See Also: → 1551677
Pushed by jteh@mozilla.com:
https://hg.mozilla.org/integration/autoland/rev/28b9af9efee6
If the focused accessible is removed from the tree, fire a11y focus on the document. r=eeejay
Status: NEW → RESOLVED
Closed: 3 months ago
Resolution: --- → FIXED
Target Milestone: --- → mozilla68
Regressions: 1551825
You need to log in before you can comment on or make changes to this bug.