mirror of
https://github.com/twbs/bootstrap.git
synced 2022-11-09 12:25:43 -05:00
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
This commit is contained in:
parent
3b3366e1b6
commit
6d64afe508
4 changed files with 12 additions and 26 deletions
|
@ -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.
|
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.
|
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.
|
||||||
|
|
||||||
|
|
|
@ -43,13 +43,11 @@ const Dropdown = (($) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClassName = {
|
const ClassName = {
|
||||||
BACKDROP : 'dropdown-backdrop',
|
|
||||||
DISABLED : 'disabled',
|
DISABLED : 'disabled',
|
||||||
SHOW : 'show'
|
SHOW : 'show'
|
||||||
}
|
}
|
||||||
|
|
||||||
const Selector = {
|
const Selector = {
|
||||||
BACKDROP : '.dropdown-backdrop',
|
|
||||||
DATA_TOGGLE : '[data-toggle="dropdown"]',
|
DATA_TOGGLE : '[data-toggle="dropdown"]',
|
||||||
FORM_CHILD : '.dropdown form',
|
FORM_CHILD : '.dropdown form',
|
||||||
MENU : '.dropdown-menu',
|
MENU : '.dropdown-menu',
|
||||||
|
@ -107,16 +105,13 @@ const Dropdown = (($) => {
|
||||||
return false
|
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 &&
|
if ('ontouchstart' in document.documentElement &&
|
||||||
!$(parent).closest(Selector.NAVBAR_NAV).length) {
|
!$(parent).closest(Selector.NAVBAR_NAV).length) {
|
||||||
|
$('body').children().on('mouseover', '*', $.noop)
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.focus()
|
this.focus()
|
||||||
|
@ -192,10 +187,10 @@ const Dropdown = (($) => {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove backdrop only if the dropdown menu will be hidden
|
// if this is a touch-enabled device we remove the extra
|
||||||
const backdrop = $(parent).find(Selector.BACKDROP)[0]
|
// empty mouseover listeners we added for iOS support
|
||||||
if (backdrop) {
|
if ('ontouchstart' in document.documentElement) {
|
||||||
backdrop.parentNode.removeChild(backdrop)
|
$('body').children().off('mouseover', '*', $.noop)
|
||||||
}
|
}
|
||||||
|
|
||||||
toggles[i].setAttribute('aria-expanded', 'false')
|
toggles[i].setAttribute('aria-expanded', 'false')
|
||||||
|
|
|
@ -135,16 +135,6 @@
|
||||||
white-space: nowrap; // as with > li > a
|
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)
|
// Allow for dropdowns to go bottom up (aka, dropup-menu)
|
||||||
//
|
//
|
||||||
// Just add .dropup after the standard .dropdown class and you're set.
|
// Just add .dropup after the standard .dropdown class and you're set.
|
||||||
|
|
|
@ -555,7 +555,6 @@ $dropdown-header-color: $gray-light !default;
|
||||||
// Warning: Avoid customizing these values. They're used for a bird's eye view
|
// 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.
|
// of components dependent on the z-axis and are designed to all work together.
|
||||||
|
|
||||||
$zindex-dropdown-backdrop: 990 !default;
|
|
||||||
$zindex-dropdown: 1000 !default;
|
$zindex-dropdown: 1000 !default;
|
||||||
$zindex-sticky: 1020 !default;
|
$zindex-sticky: 1020 !default;
|
||||||
$zindex-fixed: 1030 !default;
|
$zindex-fixed: 1030 !default;
|
||||||
|
|
Loading…
Add table
Reference in a new issue