Opened 8 years ago

Closed 7 years ago

Last modified 6 years ago

#15370 closed enhancement (fixed)

Dialog: can still focus items behind dialog

Reported by: Paul Christopher Owned by: bill
Priority: undecided Milestone: 1.9
Component: Dijit Version: 1.7.2
Keywords: Cc:
Blocked By: Blocking:

Description

Description

http://www.w3.org/TR/wai-aria-practices/#modal_dialog suggests to trap focus inside a simulated modal dialog (already implemented in dijit) and to set focus back to the dialog if focus gets lost (not yet implemented). http://www.w3.org/TR/WCAG20-TECHS/SCR37.html suggests to close the dialog in case focus gets lost.

Aside the two mentioned different "solutions", doing nothing on focus loss (as it is now) could be prone to some problems for keyboard based navigation/ sequential navigation using the tab key. The attached test case and the following "test sequences" illustrate these basic issues/points:

Test sequence 1: Run the attached test case. Open the dialog by clicking on the button "Show Dialog1". Make the dialog loose focus by either clicking in the location/address bar of the browser or by activating another page element via accesskey 1 or 3. Start to navigate the page with the tab key. All navigation will happen in the background (behind the underlay) thus being merely perceivible for a sighted person. When reaching the dialog box, focus will be trapped again in the dialog.

Test sequence 2: Again run the attached test case. Open dialog1, then click on "Show Dialog2" so as to open another dialog on top of the other dialog. Again, make Dialog2 loose focus by clicking in the address bar or by using an accesskey (or by adding another widget to the page, which takes the focus after some while so as to do something). Start to navigate the page with the tab key. All naviation actions will happen in the background. When reaching the dialogs in the tab order, focus will be trapped in Dialog1 [sic!] and not Dialog2 (although Dialog2 is the active dialog).

Discussion

Tab based navigation in the background behind the underlay is merely perceivable. It breaks the "modality" of the dialog box. Therefore the W3C standards suggest to either 1) set focus back to the dialog or 2) to close the dialog if focus gets lost.

The fact that focus is trapped in Dialog2 and not Dialog1 seems to be a minor, minor bug (but a bug in my eyes).

Solution

Since Dojo is such a superb toolkit, fixing the focus loss problem could be implemented relatively easy, e.g. by something like this (??) somwhere in Dialog.js (??):

var self = this;
focus.watch("curNode", function(){
  var curNode = focus.get('curNode');
  // Allow to focus "null" or nodes inside the dialog only otherwise do something
  if(curNode && !dom.isDescendant(curNode, self.domNode) && DialogLevelManager.isTop(self)){
     console.log(self.title + ' lost focus. Refocusing dialog/ closing dialog...');
  }
});	

Miscellaneous

I personally don't know what to do on focus loss: Closing the dialog or refocusing the dialog?

Futhermore: Is it necessary to temporarily suspend all accesskey when the dialog is open so as to make the dialog really modal (i.e. remove the accesskey attributes form the dom elements and save them to a data-dojo-accesskey attribute, so as to be able to restore them again if the dialog gets closed?).

Attachments (3)

testModalDialog.html (2.5 KB) - added by Paul Christopher 8 years ago.
[Patch_CLA]DialogDiff1-8-0.diff (871 bytes) - added by Paul Christopher 7 years ago.
Patch for Dialog.js (against 1.8.0; CLA on file)
CombinedApproach.diff (2.4 KB) - added by Paul Christopher 7 years ago.
patch for dijit/dialog.js, version 1.8.0 (CLA on file)

Download all attachments as: .zip

Change History (42)

Changed 8 years ago by Paul Christopher

Attachment: testModalDialog.html added

comment:1 Changed 8 years ago by bill

We put a lot of effort into making Dialog modal, so that tabbing stays within the dialog, but it's true we didn't handle accesskeys.

I like the idea to detect defocus and react by refocusing the dialog, although I worry about side effects of momentarily focusing another element on the page. For example, what if focusing the element causes some action to happen, like submitting a form? I'm not an expert on accesskeys and how they are typically used.

I wonder if we could somehow block the accesskeys from getting the event, but I don't think it would be possible on IE6-8. Like you said, we could temporarily disable all the access keys on the page.

comment:2 Changed 8 years ago by Paul Christopher

Using accesskeys is just one way how a dialog could loose its focus. Clicking in the adressbar of the browser and tabbing back into the page is another one. Other widgets on the page stealing focus "by accident" might be a third reason.

All I wanted to say is this: The above mentioned W3C standards give advice not only to trap focus but also what to do in case of focus loss "by accident". Doing nothing causes the issues described above.

Maybe just refocusing the dialog on focus loss is enough? Or should the dialog be closed in case another widget takes control? Should the dialog be truly modal, i.e. if a dialog is open, the user cannot trigger the accesskey to e.g. navigate back to the main page, since the dialog box forces the user to input something important?

I don't know... Maybe it depends on the use case and maybe there is no "one best" solution. Even the web standards give different advice (having different use cases in mind?): You can close the dialog or refocus it. The accesskey problem isn't even mentioned in the standards. Futhermore: As far as accesskey are concerned: The accessibility community is divided: some think it is a great concept to work easily with a page, others say that in most browser accesskeys are too complicated to activate for physically disabled persons and/or accesskey clash with short cut keys of existing assistive techologies (such as screenreaders).

comment:3 Changed 7 years ago by Paul Christopher

http://bugs.dojotoolkit.org/ticket/12479 is a duplicate of this ticket.

comment:4 Changed 7 years ago by Paul Christopher

http://bugs.dojotoolkit.org/ticket/15971 is a duplicate of this ticket: Clicking on the overlay of the dialog is another way of causing the dialog box to loose focus.

comment:5 Changed 7 years ago by bill

#15971 is a duplicate of this ticket.

Changed 7 years ago by Paul Christopher

Patch for Dialog.js (against 1.8.0; CLA on file)

comment:6 Changed 7 years ago by Paul Christopher

Added a proposal for a patch. At the least these small changes to Dilaog.js seem to do the trick (tested with the above test case). Accesskeys do not work anymore in IE, since IE only focuses the link with the accesskey. One need to press <enter> to really execute it. In contrast, accesskeys in Firefix still work: Accesskeys directly execute a link.

comment:7 Changed 7 years ago by bill

Milestone: tbd1.9

Yup, the approach in your patch seems good, although I'm going to change it a bit to only have one listener for the page, "owned" by the DialogLevelManager.

Also, it would be more user-friendly to focus the previously focused field (in oldNode), but that gets complicated because it might be in a drop down that got hidden etc., so I'll stick to your approach for simplicity.

comment:8 Changed 7 years ago by bill

Owner: set to bill
Status: newassigned

PS: Actually, it will require some more work. dom.isDescendant() isn't an appropriate check because it gets confused when focus is moved to a dropdown from a widget (ex: ComboBox) inside of the Dialog.

I'll try to get it working though.

comment:9 Changed 7 years ago by Paul Christopher

Yes, you are perfectly right. I haven't thought about widgets that have parts which are not part of the dialog (_HasDropDown etc.). And having just one listener is even better. But I did not know how to achieve this.

Regarding access keys: For a perfectly modal dialog, one needs maybe search the DOM for access key attributes remove them and store them on a data-attribute when the overlay is displayed? When the overlay is closed, the DialogLevelManger needs to restore the accesskeys. But that is a detail on which I could not find any hint in any specs (WAI ARIA, WAI WCAG). Maybe mikeb can help on that?

comment:10 Changed 7 years ago by bill

OK, well it seems like access keys would be automatically handled in the same way as the other situations, because using an access key would shift the focus outside the dialog and thus trigger the general code that we've discussed above.

And if not, then access keys could possibly be handled by listeners for keypress or keydown that call evt.preventDefault(). The listeners would need to be setup though not only on Dialog.domNode but also for dropdowns.

In any case, let's not worry about access keys for now. I have some working code that handles clicking a blank area of the viewport as well as tabbing into the page from the address bar, so I'll check that in.

comment:11 Changed 7 years ago by bill

Resolution: fixed
Status: assignedclosed

In [29823]:

If focus is accidentally lost from a Dialog, then restore it. Fixes #15370 !strict.

comment:12 Changed 7 years ago by Paul Christopher

I have just tested the above fix. As far as I can see, it seems not to work always reliably. E.g. if you click in the addressbar of the browser, focus is removed, the blur event fires but is unalbe to restore focus since the browser prevents that and keept the focus in the addressbar. But if you now start navigating through the page again using the tab key, there is no way to restore the focus back to the dialog: You step through the page behind the underlay.

I have now tried to combine both approaches: The wonderful unblur approach, which also makes sure that comboboxes work, the focus tracker (now as a singleton in the DialogLevelManager) that restores focus back to the topmost dialog, see CombinedApproach.diff.

I think, it should be bulletproof now: It works with several dialogs, when clicking in the addressbar or overlay, using a combobox...

Changed 7 years ago by Paul Christopher

Attachment: CombinedApproach.diff added

patch for dijit/dialog.js, version 1.8.0 (CLA on file)

comment:13 Changed 7 years ago by bill

Hmm, well on my tests, with the existing code, when I clicked the address bar and then tabbed back into the browser, focus was sent directly to the dialog. Where exactly did you see this scenario not working?

comment:14 Changed 7 years ago by bill

PS: your diff files are a bit difficult to use since the paths are very strange (see http://bugs.dojotoolkit.org/attachment/ticket/15370/CombinedApproach.diff which doesn't even list the file name). The tabbing is also very strange.

comment:15 Changed 7 years ago by Paul Christopher

Bill, I'm sorry. I just checked and, as you said, it works, see http://archive.dojotoolkit.org/nightly/dojotoolkit/dijit/tests/test_Dialog.html. So I did something wrong in manually applying the patch to 1.8.0. I just added the _onBlur function to dijit/Dialog.js. And that doesn't work for some reason with a freshly installed 1.8.0... Strange...

comment:16 Changed 7 years ago by bill

OK, good. Well, let me know if you find something later. Not sure why the patch doesn't apply against 1.8 (although you might as well try against the latest in the 1.8 branch, rather than 1.8.0).

comment:17 Changed 7 years ago by bill

In [29999]:

Backport [29823] to 1.8 branch, for restoring focus to Dialog when it's accidentally lost, refs #15370 !strict

comment:18 Changed 7 years ago by bill

In [30000]:

Backport [29823] to 1.7 branch, for restoring focus to Dialog when it's accidentally lost, refs #15370 !strict

comment:19 Changed 7 years ago by bill

In [30001]:

Backport [29823] to 1.6 branch, for restoring focus to Dialog when it's accidentally lost, refs #15370 !strict

comment:20 Changed 7 years ago by bill

Milestone: 1.91.6.2

Backported to 1.8.2, 1.7.5, 1.6.2.

comment:21 Changed 7 years ago by bill

In [30268]:

remove lines from bad patch application in [30001], refs #15370 !strict

comment:22 Changed 7 years ago by bill

In [30269]:

remove lines from bad patch application in [30000], refs #15370 !strict

comment:23 Changed 7 years ago by bill

#12479 is a duplicate of this ticket.

comment:24 Changed 7 years ago by bill

Milestone: 1.6.21.9
Resolution: fixed
Status: closedreopened
Summary: dijit.Dialog not really modal as suggested by http://www.w3.org/TR/wai-aria-practices/#modal_dialogDialog: can still focus items behind dialog

This fix turns out to be unstable, see #16517 and #16550 and #16551. I shouldn't have backported it. I'll rollback the backports, and leave this ticket open until context menu and iframes work again.

Last edited 7 years ago by bill (previous) (diff)

comment:25 Changed 7 years ago by bill

In [30290]:

Rollback [30001] and [30268], changes for restoring focus to Dialog when it's lost via a mouse click. Refs #15370 and fixes #16550 and #16551 on 1.6 branch, !strict.

comment:26 Changed 7 years ago by bill

In [30291]:

Rollback [30000] and [30269], changes for restoring focus to Dialog when it's lost via a mouse click. Refs #15370 and fixes #16550 and #16551 on 1.7 branch, !strict.

comment:27 Changed 7 years ago by bill

In [30292]:

Rollback [29999], changes for restoring focus to Dialog when it's lost via a mouse click. Refs #15370 and fixes #16550 and #16551 on 1.8 branch, !strict.

comment:28 Changed 7 years ago by bill

In [30411]:

More conservative approach to refocusing Dialog when the underlay is clicked/focused. Instead of monitoring Dialog._onBlur, just look for click events directly on the underlay. Refs #15370 !strict.

comment:29 Changed 7 years ago by bill

Resolution: fixed
Status: reopenedclosed

In [30419]:

If user tabs in from URL bar, catch focus on the underlay and then switch it to Dialog. Fixes #15370 !strict.

comment:30 Changed 7 years ago by bill

Hmm, clicking the underlay to close a popup is suboptimal. For example, in test_Dialog.html, click the "show dialog" button, open the DateTextBox drop down, and then click the gray area to close the drop down. Focus goes to the first field of the Dialog, rather than the DateTextBox. That does keep things simple though, for eample in the case of closing nested popups.

comment:31 Changed 7 years ago by bill

I thought about backporting this but I think the change is too big, and the benefit too small. The main issue is when the user clicks the underlay, in which case they can rectify the situation by clicking the dialog. But also, an app can work around that issue by adding custom code like below to their index.html file:

dojo.connect(dijit.Dialog.prototype, "show", function(){
   this._underlayConnect = dojo.connect(dijit._underlay.domNode, "onclick", this, function(){
      this._getFocusItems(this.domNode); 
      this._firstFocusItem.focus();
   });
});

dojo.connect(dijit.Dialog.prototype, "hide", function(){
   dojo.disconnect(this._underlayConnect);
});

comment:32 Changed 7 years ago by Paul Christopher

I did some tests with the attached test case "testModalDialog.html" using trunk (revision 30450). These are the results:

  • Firefox18, Chrome24, Safari5.1.7: Click "Show Dialog 1". In dialog 1, click "Show Dialog 2". Close dialog 2 again (but do not close dialog 1). Click in the browser's address bar. Start to navigate with the tab key. Focus is not reset to dialog 1.
  • IE9: Click "Show Dialog 1". Press Alt+1 so as to trigger an accesskey. This removes focus from the dialog. Do not press return but the tab key. Focus is not reset to the dialog. You can navigate the whole page.

Concerning 1) Resetting focus works perfectly. However if a second dialog is opened and closed, something seems to be broken?

Concerning 2) This shows that we actually don't know what causes focus loss. It could be the user clicking on the underlay, it could be the user clicking in the addressbar, it could be the user executing an accesskeys, it could be another widget that claims focus etc.

comment:33 Changed 7 years ago by bill

Resolution: fixed
Status: closedreopened

Thanks, you are right about (1), I'll fix that.

As for (2) I wasn't trying to handle access keys or any random way that focus could be stolen from the dialog. If applications do something complex like that, they are on their own. I tried to handle it in [29823] but ended up breaking existing applications.

comment:34 Changed 7 years ago by bill

Resolution: fixed
Status: reopenedclosed

In [30474]:

When two dialogs are opened and then one of them is closed, make sure clicking the underlay refocus the remaining dialog, fixes #15370 !strict.

comment:35 Changed 7 years ago by bill

Resolution: fixed
Status: closedreopened

The "previous" key on iOS's software keyboard shows the same problem as access keys, where you can focus items behind the Dialog. So I decided to try (again) to check in a fix to restore focus no matter how it is lost, but to guard against doing anything when focus is shifted to a popup, even if the popup is something like a context menu (#16550).

comment:36 Changed 7 years ago by bill

Resolution: fixed
Status: reopenedclosed

In [30682]:

Fix problem when user presses "previous" button on iOS software keyboard and focus goes from Dialog back to main page. But take care not to break context menus on the Dialog: ignore focus events that occur on those menus. Fixes #15370, refs #16550 !strict.

comment:37 Changed 7 years ago by bill

In [31155]:

fix race condition in test waiting for URL to load, causing spurious failure in Dialog_a11y.html on Chrome/win, refs #15370

comment:38 Changed 6 years ago by Bill Keese <bill@…>

In a252025f2e3f016c78230130192e61a434142d33/dijit:

Error: Processor CommitTicketReference failed
Unsupported version control system "git": Can't find an appropriate component, maybe the corresponding plugin was not enabled? 

comment:39 Changed 6 years ago by Bill Keese <bill@…>

In 31a52d41e341c17bc87891edb6103e9162d06204/dijit:

Error: Processor CommitTicketReference failed
Unsupported version control system "git": Can't find an appropriate component, maybe the corresponding plugin was not enabled? 
Note: See TracTickets for help on using tickets.