MDL-70075 core: Listen for `change` in accessibleChange event
authorAndrew Nicols <andrew@nicols.co.uk>
Mon, 2 Nov 2020 01:33:05 +0000 (09:33 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Wed, 4 Nov 2020 03:27:55 +0000 (11:27 +0800)
The accessibleChange custom interaction event was only listening for
blur and focus, however some OS/browser combinations do not emit these
events until the element is explicitly blurred. This is notably
different on Firefox on some Operating Systems.

Recent changes in MDL-68167 explicitly moved the user participants page
filter module to use the accessibleChange event, which means that the
selections are now only triggered on an explicit blur when using
Firefox. This highlight a bug whereby, when the mouse is used to make a
selection, the event is not triggered until the element is blurred.

This change modifies the accessibleChange event to ignore the `change`
event where it was triggered by the keyboard and where that keybaord
event was not a [return] or [escape] keypress, but to otherwise respect
the native change event.

lib/amd/build/custom_interaction_events.min.js
lib/amd/build/custom_interaction_events.min.js.map
lib/amd/src/custom_interaction_events.js

index 2e6312a..00d449b 100644 (file)
Binary files a/lib/amd/build/custom_interaction_events.min.js and b/lib/amd/build/custom_interaction_events.min.js differ
index a603b95..6d148be 100644 (file)
Binary files a/lib/amd/build/custom_interaction_events.min.js.map and b/lib/amd/build/custom_interaction_events.min.js.map differ
index c429e92..ca22b72 100644 (file)
@@ -427,35 +427,95 @@ define(['jquery', 'core/key_codes'], function($, keyCodes) {
         var onMac = navigator.userAgent.indexOf('Macintosh') !== -1;
         var touchEnabled = ('ontouchstart' in window) || (('msMaxTouchPoints' in navigator) && (navigator.msMaxTouchPoints > 0));
         if (onMac || touchEnabled) {
         var onMac = navigator.userAgent.indexOf('Macintosh') !== -1;
         var touchEnabled = ('ontouchstart' in window) || (('msMaxTouchPoints' in navigator) && (navigator.msMaxTouchPoints > 0));
         if (onMac || touchEnabled) {
+            // On Mac devices, and touch-enabled devices, the change event seems to be handled correctly and
+            // consistently at this time.
             element.on('change', function(e) {
                 triggerEvent(events.accessibleChange, e);
             });
         } else {
             element.on('change', function(e) {
                 triggerEvent(events.accessibleChange, e);
             });
         } else {
+            // Some browsers have non-normalised behaviour for handling the selection of values in a <select> element.
+            // When using Chrome on Linux (and possibly others), a 'change' event is fired when pressing the Escape key.
+            // When using Firefox on Linux (and possibly others), a 'change' event is fired when navigating through the
+            // list with a keyboard.
+            //
+            // To normalise these behaviours:
+            // - the initial value is stored in a data attribute when focusing the element
+            // - the current value is checked against the stored initial value when and the accessibleChange event fired when:
+            // --- blurring the element
+            // --- the 'Enter' key is pressed
+            // --- the element is clicked
+            // --- the 'change' event is fired, except where it is from a keyboard interaction
+            //
+            // To facilitate the change event keyboard interaction check, the 'keyDown' handler sets a flag to ignore
+            // the change event handler which is unset on the 'keyUp' event.
+            //
+            // Unfortunately we cannot control this entirely as some browsers (Chrome) trigger a change event when
+            // pressign the Escape key, and this is considered to be the correct behaviour.
+            // Chrome https://bugs.chromium.org/p/chromium/issues/detail?id=839717
+            //
+            // Our longer-term solution to this should be to switch away from using <select> boxes as a single-select,
+            // and make use of a dropdown of action links like the Bootstrap Dropdown menu.
+            var setInitialValue = function(target) {
+                target.dataset.initValue = target.value;
+            };
+            var resetToInitialValue = function(target) {
+                if ('initValue' in target.dataset) {
+                    target.value = target.dataset.initValue;
+                }
+            };
+            var checkAndTriggerAccessibleChange = function(e) {
+                if (!('initValue' in e.target.dataset)) {
+                    // Some browsers trigger click before focus, therefore it is possible that initValue is undefined.
+                    // In this case it's likely that it's being focused for the first time and we should therefore not submit.
+                    return;
+                }
+
+                if (e.target.value !== e.target.dataset.initValue) {
+                    // Update the initValue when the event is triggered.
+                    // This means that if the click handler fires before the focus handler on a subsequent interaction
+                    // with the element, the currently dispalyed value will be the best guess current value.
+                    e.target.dataset.initValue = e.target.value;
+                    triggerEvent(events.accessibleChange, e);
+                }
+            };
             var nativeElement = element.get()[0];
             // The `focus` and `blur` events do not support bubbling. Use Event Capture instead.
             nativeElement.addEventListener('focus', function(e) {
             var nativeElement = element.get()[0];
             // The `focus` and `blur` events do not support bubbling. Use Event Capture instead.
             nativeElement.addEventListener('focus', function(e) {
-                $(e.target).data('initValue', e.target.value);
+                setInitialValue(e.target);
             }, true);
             nativeElement.addEventListener('blur', function(e) {
             }, true);
             nativeElement.addEventListener('blur', function(e) {
-                var initValue = $(e.target).data('initValue');
-                $(e.target).removeData('initValue');
-                if (e.target.value !== initValue) {
-                    triggerEvent(events.accessibleChange, e);
-                }
+                checkAndTriggerAccessibleChange(e);
             }, true);
             element.on('keydown', function(e) {
             }, true);
             element.on('keydown', function(e) {
-                if ((e.which === keyCodes.enter) && e.target.value !== $(e.target).data('initValue')) {
-                    triggerEvent(events.accessibleChange, e);
+                if ((e.which === keyCodes.enter)) {
+                    checkAndTriggerAccessibleChange(e);
                 } else if (e.which === keyCodes.escape) {
                 } else if (e.which === keyCodes.escape) {
-                    e.target.value = $(e.target).data('initValue');
+                    resetToInitialValue(e.target);
+                    e.target.dataset.ignoreChange = true;
+                } else {
+                    // Firefox triggers a change event when using the keyboard to scroll through the selection.
+                    // Set a data- attribute that the change listener can use to ignore the change event where it was
+                    // generated from a keyboard change such as typing to complete a value, or using arrow keys.
+                    e.target.dataset.ignoreChange = true;
+
                 }
             });
                 }
             });
-            element.on('click', function(e) {
-                var initValue = $(e.target).data('initValue');
-                // Some browsers trigger onclick before onblur, therefore it is possible that initValue is undefined.
-                if (typeof initValue !== 'undefined' && initValue != e.target.value) {
-                    triggerEvent(events.accessibleChange, e);
+            element.on('change', function(e) {
+                if (e.target.dataset.ignoreChange) {
+                    // This change event was triggered from a keyboard change which is not yet complete.
+                    // Do not trigger the accessibleChange event until the selection is completed using the [return]
+                    // key.
+                    return;
                 }
                 }
+
+                checkAndTriggerAccessibleChange(e);
+            });
+            element.on('keyup', function(e) {
+                // The key has been lifted. Stop ignoring the change event.
+                delete e.target.dataset.ignoreChange;
+            });
+            element.on('click', function(e) {
+                checkAndTriggerAccessibleChange(e);
             });
         }
     };
             });
         }
     };