From 957b3f24e51cf37148ca8cb044b716c631ce90a2 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Tue, 19 Apr 2022 17:15:31 +0200 Subject: [PATCH 1/3] fix(dropdown): Remove most of the special handling for touch devices Instead of distinguishing between touch and non-touch devices and binding to different events, we always bind to mouse events. For touch input, we mostly rely on the mouse event emulation (esp. click and mouseenter events) of modern browsers. This fixes #1989 --- src/definitions/modules/dropdown.js | 79 +++++++++-------------------- 1 file changed, 23 insertions(+), 56 deletions(-) diff --git a/src/definitions/modules/dropdown.js b/src/definitions/modules/dropdown.js index 69f329fc63..46e36e4e84 100644 --- a/src/definitions/modules/dropdown.js +++ b/src/definitions/modules/dropdown.js @@ -30,11 +30,6 @@ $.fn.dropdown = function(parameters) { moduleSelector = $allModules.selector || '', - hasTouch = ('ontouchstart' in document.documentElement), - clickEvent = hasTouch - ? 'touchstart' - : 'click', - time = new Date().getTime(), performance = [], @@ -541,9 +536,7 @@ $.fn.dropdown = function(parameters) { } if(settings.onShow.call(element) !== false) { module.animate.show(function() { - if( module.can.click() ) { - module.bind.intent(); - } + module.bind.intent(); if(module.has.search() && !preventFocus) { module.focusSearch(); } @@ -571,7 +564,7 @@ $.fn.dropdown = function(parameters) { callback.call(element); }); } - } else if( module.can.click() ) { + } else { module.unbind.intent(); } iconClicked = false; @@ -639,8 +632,8 @@ $.fn.dropdown = function(parameters) { module.verbose('Binding mouse events'); if(module.is.multiple()) { $module - .on(clickEvent + eventNamespace, selector.label, module.event.label.click) - .on(clickEvent + eventNamespace, selector.remove, module.event.remove.click) + .on('click' + eventNamespace, selector.label, module.event.label.click) + .on('click' + eventNamespace, selector.remove, module.event.remove.click) ; } if( module.is.searchSelection() ) { @@ -649,31 +642,33 @@ $.fn.dropdown = function(parameters) { .on('mouseup' + eventNamespace, module.event.mouseup) .on('mousedown' + eventNamespace, selector.menu, module.event.menu.mousedown) .on('mouseup' + eventNamespace, selector.menu, module.event.menu.mouseup) - .on(clickEvent + eventNamespace, selector.icon, module.event.icon.click) - .on(clickEvent + eventNamespace, selector.clearIcon, module.event.clearIcon.click) + .on('click' + eventNamespace, selector.icon, module.event.icon.click) + .on('click' + eventNamespace, selector.clearIcon, module.event.clearIcon.click) .on('focus' + eventNamespace, selector.search, module.event.search.focus) - .on(clickEvent + eventNamespace, selector.search, module.event.search.focus) + .on('click' + eventNamespace, selector.search, module.event.search.focus) .on('blur' + eventNamespace, selector.search, module.event.search.blur) - .on(clickEvent + eventNamespace, selector.text, module.event.text.focus) + .on('click' + eventNamespace, selector.text, module.event.text.focus) ; if(module.is.multiple()) { $module - .on(clickEvent + eventNamespace, module.event.click) - .on(clickEvent + eventNamespace, module.event.search.focus) + .on('click' + eventNamespace, module.event.click) + .on('click' + eventNamespace, module.event.search.focus) ; } } else { if(settings.on == 'click') { $module - .on(clickEvent + eventNamespace, selector.icon, module.event.icon.click) - .on(clickEvent + eventNamespace, module.event.test.toggle) + .on('click' + eventNamespace, selector.icon, module.event.icon.click) + .on('click' + eventNamespace, module.event.test.toggle) ; } else if(settings.on == 'hover') { $module .on('mouseenter' + eventNamespace, module.delay.show) .on('mouseleave' + eventNamespace, module.delay.hide) + .on('touchstart' + eventNamespace, module.event.test.toggle) + .on('touchstart' + eventNamespace, selector.icon, module.event.icon.click) ; } else { @@ -685,7 +680,7 @@ $.fn.dropdown = function(parameters) { .on('mousedown' + eventNamespace, module.event.mousedown) .on('mouseup' + eventNamespace, module.event.mouseup) .on('focus' + eventNamespace, module.event.focus) - .on(clickEvent + eventNamespace, selector.clearIcon, module.event.clearIcon.click) + .on('click' + eventNamespace, selector.clearIcon, module.event.clearIcon.click) ; if(module.has.menuSearch() ) { $module @@ -699,21 +694,16 @@ $.fn.dropdown = function(parameters) { } } $menu - .on((hasTouch ? 'touchstart' : 'mouseenter') + eventNamespace, selector.item, module.event.item.mouseenter) + .on('mouseenter' + eventNamespace, selector.item, module.event.item.mouseenter) + .on('touchstart' + eventNamespace, selector.item, module.event.item.mouseenter) .on('mouseleave' + eventNamespace, selector.item, module.event.item.mouseleave) .on('click' + eventNamespace, selector.item, module.event.item.click) ; }, intent: function() { module.verbose('Binding hide intent event to document'); - if(hasTouch) { - $document - .on('touchstart' + elementNamespace, module.event.test.touch) - .on('touchmove' + elementNamespace, module.event.test.touch) - ; - } $document - .on(clickEvent + elementNamespace, module.event.test.hide) + .on('click' + elementNamespace, module.event.test.hide) ; } }, @@ -721,14 +711,8 @@ $.fn.dropdown = function(parameters) { unbind: { intent: function() { module.verbose('Removing hide intent event from document'); - if(hasTouch) { - $document - .off('touchstart' + elementNamespace) - .off('touchmove' + elementNamespace) - ; - } $document - .off(clickEvent + elementNamespace) + .off('click' + elementNamespace) ; } }, @@ -1267,23 +1251,12 @@ $.fn.dropdown = function(parameters) { if (!module.is.multiple() || (module.is.multiple() && !module.is.active())) { focused = true; } - if( module.determine.eventOnElement(event, toggleBehavior) ) { + if( module.determine.eventOnElement(event, toggleBehavior) && event.type !== 'touchstart') { + // do not preventDefault of touchstart; so emulated mouseenter is triggered on first touch and not later + // (when selecting an item). The double-showing of the dropdown through both events does not hurt. event.preventDefault(); } }, - touch: function(event) { - module.determine.eventOnElement(event, function() { - if(event.type == 'touchstart') { - module.timer = setTimeout(function() { - module.hide(); - }, settings.delay.touch); - } - else if(event.type == 'touchmove') { - clearTimeout(module.timer); - } - }); - event.stopPropagation(); - }, hide: function(event) { if(module.determine.eventInModule(event, module.hide)){ if(element.id && $(event.target).attr('for') === element.id){ @@ -3607,9 +3580,6 @@ $.fn.dropdown = function(parameters) { $currentMenu.removeClass(className.loading); return canOpenRightward; }, - click: function() { - return (hasTouch || settings.on == 'click'); - }, extendSelect: function() { return settings.allowAdditions || settings.apiSettings; }, @@ -3679,9 +3649,7 @@ $.fn.dropdown = function(parameters) { start = ($subMenu) ? function() {} : function() { - if( module.can.click() ) { - module.unbind.intent(); - } + module.unbind.intent(); module.remove.active(); }, transition = settings.transition.hideMethod || module.get.transition($subMenu) @@ -4058,7 +4026,6 @@ $.fn.dropdown.settings = { hide : 300, show : 200, search : 20, - touch : 50 }, /* Callbacks */ From bba81e3c739e8268a40b1cdd3ae6497a85589356 Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Tue, 19 Apr 2022 17:19:35 +0200 Subject: [PATCH 2/3] fix(dropdown): Fix hiding of nested submenus Before this fix, nested sub-menus/dropdowns with more than two menu layers would not hide correctly when moving the mouse cursor from one menu directly to a "grand-parent" or higher menu (e.g. from the most inner to the most outer menu in a three layer menu). --- src/definitions/modules/dropdown.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/definitions/modules/dropdown.js b/src/definitions/modules/dropdown.js index 46e36e4e84..54df5b9c9a 100644 --- a/src/definitions/modules/dropdown.js +++ b/src/definitions/modules/dropdown.js @@ -1340,13 +1340,15 @@ $.fn.dropdown = function(parameters) { }, mouseleave: function(event) { var - $subMenu = $(this).children(selector.menu) + $subMenu = $(this).find(selector.menu) ; if($subMenu.length > 0) { clearTimeout(module.itemTimer); module.itemTimer = setTimeout(function() { module.verbose('Hiding sub-menu', $subMenu); - module.animate.hide(false, $subMenu); + $subMenu.each(function() { + module.animate.hide(false, $(this)); + }); }, settings.delay.hide); } }, From 26a8fa1f1ef45f06024a3f339ee763add940702c Mon Sep 17 00:00:00 2001 From: Michael Thies Date: Tue, 19 Apr 2022 17:24:12 +0200 Subject: [PATCH 3/3] fix(dropdown): Hide submenus reliably, even with slow mouseleave detection Some browsers seem to trigger mouse leave events only when moving the mouse cursor. I noticed that on mobile browsers, especially when using the touch screen. This somehow causes the submenus to stay visible even when the root menu is hidden (e.g. due to clicking/selecting a menu item). Thus, this commit forces hiding of all nested submenus, when hiding a menu. --- src/definitions/modules/dropdown.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/definitions/modules/dropdown.js b/src/definitions/modules/dropdown.js index 54df5b9c9a..85b0e97fa1 100644 --- a/src/definitions/modules/dropdown.js +++ b/src/definitions/modules/dropdown.js @@ -563,6 +563,15 @@ $.fn.dropdown = function(parameters) { } callback.call(element); }); + // Hide submenus explicitly. On some browsers (esp. mobile), they will not automatically receive a + // mouseleave event + var $subMenu = $module.find(selector.menu); + if($subMenu.length > 0) { + module.verbose('Hiding sub-menu', $subMenu); + $subMenu.each(function() { + module.animate.hide(false, $(this)); + }); + } } } else { module.unbind.intent();