From 6d64afe508bfd0bcfb5831a9a4708cef4ad88334 Mon Sep 17 00:00:00 2001
From: "Patrick H. Lauke" <redux@splintered.co.uk>
Date: Fri, 14 Apr 2017 09:19:00 +0100
Subject: [PATCH] Replace dropdown backdrop hack with cleaner JS-only hack

* Replace backdrop with simple noop mouse listener

As discussed in https://github.com/twbs/bootstrap/pull/22422 the current
approach of injecting a backdrop (to work around iOS' broken event
delegation for the `click` event) has annoying consequences on
touch-enabled laptop/desktop devices.
Instead of a backdrop `<div>`, here we simply add extra empty/noop
mouse listeners to the immediate children of `<body>` (and remove
them when the dropdown is closed) in order to force iOS to properly
bubble a `click` resulting from a tap (essentially, method 2 from
https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html)
This is sufficient (except in rare cases where the user does manage to tap
on the body itself, rather than any child elements of body - which is not
very likely in an iOS phone/tablet scenario for most layouts) to get iOS to
get a grip and do the correct event bubbling/delegation, meaning the regular
"click" event will bubble back to the `<body>` when tapping outside of the dropdown,
and the dropdown will close properly (just like it already does, even without
this fix, in non-iOS touchscreen devices/browsers, like Chrome/Android and
Windows on a touch laptop).
This approach, though a bit hacky, has no impact on the DOM structure, and
has no unforeseen side effects on touch-enabled laptops/desktops. And crucially,
it works just fine in iOS.

* Remove dropdown backdrop styles

* Update doc for dropdowns and touch-enabled devices
---
 docs/components/dropdowns.md |  4 +++-
 js/src/dropdown.js           | 23 +++++++++--------------
 scss/_dropdown.scss          | 10 ----------
 scss/_variables.scss         |  1 -
 4 files changed, 12 insertions(+), 26 deletions(-)

diff --git a/docs/components/dropdowns.md b/docs/components/dropdowns.md
index bb54c040e9..0be8d4d74c 100644
--- a/docs/components/dropdowns.md
+++ b/docs/components/dropdowns.md
@@ -480,7 +480,9 @@ Add `.disabled` to items in the dropdown to **style them as disabled**.
 
 Via data attributes or JavaScript, the dropdown plugin toggles hidden content (dropdown menus) by toggling the `.show` class on the parent list item.
 
-On touch-enabled devices, opening a dropdown adds a `.dropdown-backdrop` as a tap area for closing dropdown menus when tapping outside the menu, to work around a quirk in iOS' event delegation. **This means that once a dropdown menu is open, any tap or click (including with a mouse, on a multi-input device such as a laptop with a touchscreen) outside of the menu will be intercepted to close the menu. Opening another dropdown menu, or activating any other control or link, will therefore require an extra tap or click.**
+{% callout info %}
+On touch-enabled devices, opening a dropdown adds empty (`$.noop`) `mouseover` handlers to the immediate children of the `<body>` element. This admittedly ugly hack is necessary to work around a [quirk in iOS' event delegation](https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html), which would otherwise prevent a tap anywhere outside of the dropdown from triggering the code that closes the dropdown. Once the dropdown is closed, these additional empty `mouseover` handlers are removed.
+{% endcallout %}
 
 Note: The `data-toggle="dropdown"` attribute is relied on for closing dropdown menus at an application level, so it's a good idea to always use it.
 
diff --git a/js/src/dropdown.js b/js/src/dropdown.js
index b616186f32..812e718a88 100644
--- a/js/src/dropdown.js
+++ b/js/src/dropdown.js
@@ -43,13 +43,11 @@ const Dropdown = (($) => {
   }
 
   const ClassName = {
-    BACKDROP : 'dropdown-backdrop',
     DISABLED : 'disabled',
     SHOW     : 'show'
   }
 
   const Selector = {
-    BACKDROP      : '.dropdown-backdrop',
     DATA_TOGGLE   : '[data-toggle="dropdown"]',
     FORM_CHILD    : '.dropdown form',
     MENU          : '.dropdown-menu',
@@ -107,16 +105,13 @@ const Dropdown = (($) => {
         return false
       }
 
-      // set the backdrop only if the dropdown menu will be opened
+      // if this is a touch-enabled device we add extra
+      // empty mouseover listeners to the body's immediate children;
+      // only needed because of broken event delegation on iOS
+      // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
       if ('ontouchstart' in document.documentElement &&
          !$(parent).closest(Selector.NAVBAR_NAV).length) {
-
-        // if touch-enabled device we use a backdrop because click events
-        // don't delegate on iOS - see https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
-        const backdrop     = document.createElement('div')
-        backdrop.className = ClassName.BACKDROP
-        $(backdrop).insertBefore(this)
-        $(backdrop).on('click', Dropdown._clearMenus)
+        $('body').children().on('mouseover', '*', $.noop)
       }
 
       this.focus()
@@ -192,10 +187,10 @@ const Dropdown = (($) => {
           continue
         }
 
-        // remove backdrop only if the dropdown menu will be hidden
-        const backdrop = $(parent).find(Selector.BACKDROP)[0]
-        if (backdrop) {
-          backdrop.parentNode.removeChild(backdrop)
+        // if this is a touch-enabled device we remove the extra
+        // empty mouseover listeners we added for iOS support
+        if ('ontouchstart' in document.documentElement) {
+          $('body').children().off('mouseover', '*', $.noop)
         }
 
         toggles[i].setAttribute('aria-expanded', 'false')
diff --git a/scss/_dropdown.scss b/scss/_dropdown.scss
index 116a84a3b8..3c5a5c66ed 100644
--- a/scss/_dropdown.scss
+++ b/scss/_dropdown.scss
@@ -135,16 +135,6 @@
   white-space: nowrap; // as with > li > a
 }
 
-// Backdrop to catch body clicks on mobile, etc.
-.dropdown-backdrop {
-  position: fixed;
-  top: 0;
-  right: 0;
-  bottom: 0;
-  left: 0;
-  z-index: $zindex-dropdown-backdrop;
-}
-
 // Allow for dropdowns to go bottom up (aka, dropup-menu)
 //
 // Just add .dropup after the standard .dropdown class and you're set.
diff --git a/scss/_variables.scss b/scss/_variables.scss
index 957e69d5c2..dd5062b8bd 100644
--- a/scss/_variables.scss
+++ b/scss/_variables.scss
@@ -555,7 +555,6 @@ $dropdown-header-color:          $gray-light !default;
 // Warning: Avoid customizing these values. They're used for a bird's eye view
 // of components dependent on the z-axis and are designed to all work together.
 
-$zindex-dropdown-backdrop:  990 !default;
 $zindex-dropdown:           1000 !default;
 $zindex-sticky:             1020 !default;
 $zindex-fixed:              1030 !default;
-- 
GitLab