CSPACE 3304 - dismissing popup areas on IE8
Antranig Basman
antranig.basman at colorado.edu
Thu Dec 16 08:10:45 UTC 2010
This mail contains far more detail than most readers would want to go into, but more than anything provides
a repository for future reference and analysis should we ever run into an issue similar to this again (or
indeed, as seems likely, continue running into *this* issue :P)
Over the last few of days, Yura and I have been investigating issues involving popup areas (in particular
NumberPatternChooser and Autocomplete) that have been behaving incorrectly under IE8. This involves tracking
of focus and blur events in a way that is consistent across all browsers and is usually unrewardingly
intricate. We have two main components in CSpace which deal with this, according to different strategies.
Actually until yesterday I wasn't clear that these were really aimed at essentially the *same* purpose. The
first, "GlobalDismissal", is something fairly quick & simple I put in during my first month on CSpace in
August to deal with cases where various dialogs (date widget, confirmation etc.) were not being dismissed
correctly when the user clicked outside them. This initial implementation only tracked mouse events and was
not extended to deal with general focus loss. One idea we had during the attempt to fix CSPACE-3304 was to
extend "GlobalDismissal" by using the new jQuery synthetic event "focusin" which promises to allow
registering of focus handlers which bubble up through the document (browsers historically have treated this
bubbling inconsistently)
"GlobalDismissal" approach
In this approach we register a global "focusin" handler on the entire document base. In this case the popup
opens and stays open, but unfortunately on IE8 closes on receiving the first down arrow keystroke (on all
other browsers it behaves correctly). The following trace shows the sequence of focus and blur events.
*1) LOG: 17:54:20.375: Invoking blur on TR#fluid-id-334.csc-numberPatternChooser-patternRow cs-selecting
*2) LOG: 17:54:20.390: received focusin on BODY#acquisition
*3) LOG: 17:54:31.906: Invoking focus on TR#fluid-id-335.csc-numberPatternChooser-patternRow
*4) LOG: 17:54:32.172: received focusin on BODY#acquisition
The transition between *1 and *2 is apparently synchronous. This means that with the "globalDismissal"
approach the popup will already have been closed by the time the focus event arrives (these were actually
from two different keystrokes, please ignore the intervening gap of 11 seconds)
The initial "blur" results from the behaviour of the jquery.keyboard-a11y plugin which attempts to ensure
consistency by matching synthetic blur events to focus events. Unfortunately IE8 *synchronously* interprets
the blur event without waiting for a further focus on the same event stack, and immediately synthesises its
own focus based on the blur which focuses "nothing in particular", in fact the entire document body. This
implies that any "stateless" approach to tracking blur on IE8 cannot work (unless the a11y plugin is
rewritten to not generate blurs particularly on IE8 - they are unfortunately necessary on at least two other
modern browsers, FF 3.6 and Safari 4) since this intervening event will *always* be interpreted as a signal
that the contents of the popup has lost focus.
"Dead Man's Blur" approach
This is a utility which has been unofficially part of the Fluid framework for a couple of releases but in
the current 1.3 release is being upgraded to core status - especially in regard of its usage in several
places around CSpace. This registers a "stateful event processor" which, on receiving a blur on some number
of "core elements" starts a timer, waiting to see whether the reason for the blur is a shortly forthcoming
focus on ANOTHER of these elements. If this focus is received, the forthcoming blur handler (probably
dismissing a dialog or popup of some kind) is cancelled. If however the timer expires without such an event,
the originally queued blur handler is executed. In the following trace, the default expiration timer is set
to 150ms:
*01) LOG: 19:17:01.250: received focusin on DIV#secondary-nav-menu.csc-tabs-container
*02) LOG: 19:17:02.812: received focusin on INPUT#.csc-acquisition-numberPatternChooser-reference-number
*03) LOG: 19:17:04.109: Cancelled by focus on INPUT.csc-numberPatternChooser-button
*04) LOG: 19:17:04.109: received focusin on INPUT.csc-numberPatternChooser-button
*05) LOG: 19:17:05.250: focusing list
*06) LOG: 19:17:05.250: received focusin on TABLE.csc-numberPatternChooser-list
*07) LOG: 19:17:05.250: Cancelled by focus on INPUT.csc-numberPatternChooser-button
*08) LOG: 19:17:05.265: Beginning timer for blur on INPUT.csc-numberPatternChooser-button
*09) LOG: 19:17:05.281: Container focus handler executing
*10) LOG: 19:17:05.281: focusing item TR.csc-numberPatternChooser-patternRow
*11) LOG: 19:17:05.422: Timer expired, blurring <=== POPUP CLOSED HERE
*12) LOG: 19:17:05.422: received focusin on DIV.csc-numberPatternChooser cs-numberPatternChooser
*13) LOG: 19:17:05.422: Beginning timer for blur on TR.csc-numberPatternChooser-patternRow cs-selecting
*14) LOG: 19:17:05.578: Timer expired, blurring
*01), *02) General startup, mouse click on the entry field
*03) TAB key to tbutton
*05) SPACE activates button - list is focused
*07), *08) Anomalous *REFOCUS* and blur of button by IE event handling machinery which cannot really be
explained, but *should* be essentially harmless, if it were not for:
*09), *10), *11) Container focus handler executes for selectable row elements - which *SHOULD* have resulted
in a focusin event after *10) resulting in cancellation of the blur handler. Instead it simply expires,
closing the dialog.
*12), *13), *14) After the disaster of inappropriately closing the popup, various other inappropriate events
are generated by IE8, including the processing of an "out-of-order" focus followed by blur.
The core issue appears to be the "failure of faithfulness" in IE8 of the jQuery focus() call executed on the
pattern row element. One would expect this either synchronously or within a short period of time to give
rise to an event which would be caught by jQuery's own "focusin" handler but it does not. This is an issue
encountered in various test cases in Fluid but was until now dealt with in an "ad hoc" manner to make the
tests pass correctly under IE8. This is presumably a jQuery framework bug, but in the meantime it appears
that bypassing jQuery for programmatic focus and blur calls does indeed give rise to the expected
"cancellation event" required by dead man's blur:
*01) LOG: 19:51:26.750: received focusin on DIV.csc-numberPatternChooser cs-numberPatternChooser
*02) LOG: 19:51:27.828: received focusin on INPUT#.csc-acquisition-numberPatternChooser-reference-number
*03) LOG: 19:51:29.297: Cancelled by focus on INPUT.csc-numberPatternChooser-button
*04) LOG: 19:51:29.297: received focusin on INPUT.csc-numberPatternChooser-button
*05) LOG: 19:51:30.640: focusing list
*06) LOG: 19:51:30.640: received focusin on TABLE.csc-numberPatternChooser-list
*07) LOG: 19:51:30.640: Cancelled by focus on INPUT.csc-numberPatternChooser-button
*08) LOG: 19:51:30.656: Beginning timer for blur on INPUT.csc-numberPatternChooser-button
*09) LOG: 19:51:30.656: Container focus handler executing
*10) LOG: 19:51:30.656: focusing item TR.csc-numberPatternChooser-patternRow
*11) LOG: 19:51:30.656: Cancelled by focus on TR.csc-numberPatternChooser-patternRow (SUCCESSFUL FOCUSIN)
*12) LOG: 19:51:30.656: received focusin on TR.csc-numberPatternChooser-patternRow
*13) LOG: 19:51:30.812: Timer expired, blurring
<==== DOWN ARROW PRESSED HERE
*14) LOG: 19:51:35.562: Invoking blur on TR.csc-numberPatternChooser-patternRow cs-selecting
*15) LOG: 19:51:35.562: received focusin on BODY#acquisition
*16) LOG: 19:51:35.562: Invoking focus on TR.csc-numberPatternChooser-patternRow
*17) LOG: 19:51:35.562: Cancelled by focus on TR.csc-numberPatternChooser-patternRow
*18) LOG: 19:51:35.562: received focusin on TR.csc-numberPatternChooser-patternRow
*19) LOG: 19:51:35.578: Beginning timer for blur on TR.csc-numberPatternChooser-patternRow
*20) LOG: 19:51:35.734: Timer expired, blurring
*21) LOG: 19:51:35.734: received focusin on DIV.csc-numberPatternChooser cs-numberPatternChooser
*22) LOG: 19:51:35.734: Beginning timer for blur on TR.csc-numberPatternChooser-patternRow cs-selecting
*23) LOG: 19:51:35.890: Timer expired, blurring
Unfortunately, to add insult to injury, this trace reveals a FURTHER problem. Starting the further train of
events after *14), the user presses the down arrow to change the selection of the number pattern. This
schedules a "blur" train which as seen in our first trace causes a synchronous blur and then focus on the
further element. Unfortunately, as *18), *19) reveal, the ORIGINAL blur event continues to propagate
asynchronously, not setting off the timer in deadMan'sBlur until *AFTER* the focus event which might cancel
it is received.
Dealing with this requires an awkward "proleptic" extension to dead man's blur. It needs to keep track of
timestamps for cancellation events and match them up with blurs occuring in time windows BOTH BEFORE AND
AFTER the blur handler starts to execute.
Implementing this extension allows NumberPatternChooser to work....
However, for Autocomplete, there is a yet further insult:
LOG: 23:59:02.844: Applying raw focus to LI#matchItem:.cs-autocomplete-matchItem
is followed on expiry of the timer by
LOG: 23:59:02.969: New value
In this case we have met our match.... even raw triggering of the native "focus" event on IE fails to catch
the global "focusin" handler. The only way out is to abandon direct manipulation of "focus" events entirely
and declare our own totally fresh events "fluid-focus" and "fluid-blur" which we ensure to fire IN ADDITION
to the standard jQuery events, but which are guaranteed to propagate up the DOM tree reliably by virtue of
the standard jQuery event mechanism. We then listen to these events in addition to the DOM standard ones.
IN SUMMARY, we identify 3 bugs in the IE8/jQuery system:
BUG 1: IE8 will fire a synchronous FOCUS event (momentarily targetted at the docuemtn) on being given a
synthetic blur event
BUG 2: IE8 will not notify HANDLERS to the blur event synchronously, but wait until the FOCUS event handler
stack returns, this allowing observers to see "out-of-order" service of focus and blur events.
BUG 3: Causing synthetic focus with jQuery's "focus" call is not "faithful" on IE and will not give rise to
any kind of focus handler notification at all (looks like neither focus nor focusin). Only a call to the raw
DOM node's "focus" method will achieve this (and in practice, this has also now been found unreliable).
Fixing bug 3 requires patching the keyboard-a11y plugin to apply raw DOM methods (now - new synthetic focus
event via fluid.focus()) for synthetic focus wherever it can.
The presence of bug 1 implies that "globalDismissal" cannot be used since it is insufficiently context aware.
Fixing bug 2 requires creation of a "proleptic dead man's blur" which will allow blur handlers to be
preemptively cancelled by focus events registered in a short time window BEFORE the blur handler itself
starts to execute.
These fixes are now attached to the base FLUID JIRA
http://issues.fluidproject.org/browse/FLUID-3487
The patch version "i" was applied to trunk yesterday and will be part of the Infusion 1.3 final release.
This appeared stable and passed many test cases, but turned out not to be sufficient to resolve CSPACE-3304.
Patch version "j" is required to support the behaviour described above on IE8.
The fixes on the CollectionSpace side are attached to the original
http://issues.collectionspace.org/browse/CSPACE-3304 as "CSPACE-3304-proleptic-DMB.patch"
These patches are in a "nearly done" state but still include a lot of logging statements to aid debugging on
other machines and platforms. Hopefully during the course of the day they will be checked and verified by
others.
More information about the fluid-work
mailing list