diff options
| author | Patrick H. Lauke <[email protected]> | 2017-04-14 09:19:00 +0100 |
|---|---|---|
| committer | GitHub <[email protected]> | 2017-04-14 09:19:00 +0100 |
| commit | 6d64afe508bfd0bcfb5831a9a4708cef4ad88334 (patch) | |
| tree | e42caa655ffa11b5ea8d4dc50340682628a9fd46 | |
| parent | 3b3366e1b6ab30acd8cb468fb6570cca1b38f01e (diff) | |
| download | bootstrap-6d64afe508bfd0bcfb5831a9a4708cef4ad88334.tar.xz bootstrap-6d64afe508bfd0bcfb5831a9a4708cef4ad88334.zip | |
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
| -rw-r--r-- | docs/components/dropdowns.md | 4 | ||||
| -rw-r--r-- | js/src/dropdown.js | 23 | ||||
| -rw-r--r-- | scss/_dropdown.scss | 10 | ||||
| -rw-r--r-- | scss/_variables.scss | 1 |
4 files changed, 12 insertions, 26 deletions
diff --git a/docs/components/dropdowns.md b/docs/components/dropdowns.md index bb54c040e..0be8d4d74 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 b616186f3..812e718a8 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 116a84a3b..3c5a5c66e 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 957e69d5c..dd5062b8b 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; |
