aboutsummaryrefslogtreecommitdiff
path: root/js/src/dropdown.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/dropdown.js')
-rw-r--r--js/src/dropdown.js300
1 files changed, 130 insertions, 170 deletions
diff --git a/js/src/dropdown.js b/js/src/dropdown.js
index 590c74801..d1f573fc8 100644
--- a/js/src/dropdown.js
+++ b/js/src/dropdown.js
@@ -1,6 +1,6 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): dropdown.js
+ * Bootstrap (v5.1.0): dropdown.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
@@ -9,14 +9,16 @@ import * as Popper from '@popperjs/core'
import {
defineJQueryPlugin,
+ getElement,
getElementFromSelector,
+ getNextActiveElement,
+ isDisabled,
isElement,
- isVisible,
isRTL,
+ isVisible,
noop,
typeCheckConfig
} from './util/index'
-import Data from './dom/data'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import SelectorEngine from './dom/selector-engine'
@@ -46,12 +48,10 @@ const EVENT_HIDE = `hide${EVENT_KEY}`
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
-const EVENT_CLICK = `click${EVENT_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`
const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`
-const CLASS_NAME_DISABLED = 'disabled'
const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_DROPUP = 'dropup'
const CLASS_NAME_DROPEND = 'dropend'
@@ -59,7 +59,6 @@ const CLASS_NAME_DROPSTART = 'dropstart'
const CLASS_NAME_NAVBAR = 'navbar'
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="dropdown"]'
-const SELECTOR_FORM_CHILD = '.dropdown form'
const SELECTOR_MENU = '.dropdown-menu'
const SELECTOR_NAVBAR_NAV = '.navbar-nav'
const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'
@@ -73,20 +72,20 @@ const PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start'
const Default = {
offset: [0, 2],
- flip: true,
boundary: 'clippingParents',
reference: 'toggle',
display: 'dynamic',
- popperConfig: null
+ popperConfig: null,
+ autoClose: true
}
const DefaultType = {
offset: '(array|string|function)',
- flip: 'boolean',
boundary: '(string|element)',
reference: '(string|element|object)',
display: 'string',
- popperConfig: '(null|object|function)'
+ popperConfig: '(null|object|function)',
+ autoClose: '(boolean|string)'
}
/**
@@ -103,8 +102,6 @@ class Dropdown extends BaseComponent {
this._config = this._getConfig(config)
this._menu = this._getMenuElement()
this._inNavbar = this._detectNavbar()
-
- this._addEventListeners()
}
// Getters
@@ -117,34 +114,21 @@ class Dropdown extends BaseComponent {
return DefaultType
}
- static get DATA_KEY() {
- return DATA_KEY
+ static get NAME() {
+ return NAME
}
// Public
toggle() {
- if (this._element.disabled || this._element.classList.contains(CLASS_NAME_DISABLED)) {
- return
- }
-
- const isActive = this._element.classList.contains(CLASS_NAME_SHOW)
-
- Dropdown.clearMenus()
-
- if (isActive) {
- return
- }
-
- this.show()
+ return this._isShown() ? this.hide() : this.show()
}
show() {
- if (this._element.disabled || this._element.classList.contains(CLASS_NAME_DISABLED) || this._menu.classList.contains(CLASS_NAME_SHOW)) {
+ if (isDisabled(this._element) || this._isShown(this._menu)) {
return
}
- const parent = Dropdown.getParentFromElement(this._element)
const relatedTarget = {
relatedTarget: this._element
}
@@ -155,37 +139,12 @@ class Dropdown extends BaseComponent {
return
}
+ const parent = Dropdown.getParentFromElement(this._element)
// Totally disable Popper for Dropdowns in Navbar
if (this._inNavbar) {
Manipulator.setDataAttribute(this._menu, 'popper', 'none')
} else {
- if (typeof Popper === 'undefined') {
- throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)')
- }
-
- let referenceElement = this._element
-
- if (this._config.reference === 'parent') {
- referenceElement = parent
- } else if (isElement(this._config.reference)) {
- referenceElement = this._config.reference
-
- // Check if it's jQuery element
- if (typeof this._config.reference.jquery !== 'undefined') {
- referenceElement = this._config.reference[0]
- }
- } else if (typeof this._config.reference === 'object') {
- referenceElement = this._config.reference
- }
-
- const popperConfig = this._getPopperConfig()
- const isDisplayStatic = popperConfig.modifiers.find(modifier => modifier.name === 'applyStyles' && modifier.enabled === false)
-
- this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig)
-
- if (isDisplayStatic) {
- Manipulator.setDataAttribute(this._menu, 'popper', 'static')
- }
+ this._createPopper(parent)
}
// If this is a touch-enabled device we add extra
@@ -195,19 +154,19 @@ class Dropdown extends BaseComponent {
if ('ontouchstart' in document.documentElement &&
!parent.closest(SELECTOR_NAVBAR_NAV)) {
[].concat(...document.body.children)
- .forEach(elem => EventHandler.on(elem, 'mouseover', null, noop()))
+ .forEach(elem => EventHandler.on(elem, 'mouseover', noop))
}
this._element.focus()
this._element.setAttribute('aria-expanded', true)
- this._menu.classList.toggle(CLASS_NAME_SHOW)
- this._element.classList.toggle(CLASS_NAME_SHOW)
+ this._menu.classList.add(CLASS_NAME_SHOW)
+ this._element.classList.add(CLASS_NAME_SHOW)
EventHandler.trigger(this._element, EVENT_SHOWN, relatedTarget)
}
hide() {
- if (this._element.disabled || this._element.classList.contains(CLASS_NAME_DISABLED) || !this._menu.classList.contains(CLASS_NAME_SHOW)) {
+ if (isDisabled(this._element) || !this._isShown(this._menu)) {
return
}
@@ -215,29 +174,12 @@ class Dropdown extends BaseComponent {
relatedTarget: this._element
}
- const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget)
-
- if (hideEvent.defaultPrevented) {
- return
- }
-
- if (this._popper) {
- this._popper.destroy()
- }
-
- this._menu.classList.toggle(CLASS_NAME_SHOW)
- this._element.classList.toggle(CLASS_NAME_SHOW)
- Manipulator.removeDataAttribute(this._menu, 'popper')
- EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)
+ this._completeHide(relatedTarget)
}
dispose() {
- EventHandler.off(this._element, EVENT_KEY)
- this._menu = null
-
if (this._popper) {
this._popper.destroy()
- this._popper = null
}
super.dispose()
@@ -252,12 +194,28 @@ class Dropdown extends BaseComponent {
// Private
- _addEventListeners() {
- EventHandler.on(this._element, EVENT_CLICK, event => {
- event.preventDefault()
- event.stopPropagation()
- this.toggle()
- })
+ _completeHide(relatedTarget) {
+ const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget)
+ if (hideEvent.defaultPrevented) {
+ return
+ }
+
+ // If this is a touch-enabled device we remove the extra
+ // empty mouseover listeners we added for iOS support
+ if ('ontouchstart' in document.documentElement) {
+ [].concat(...document.body.children)
+ .forEach(elem => EventHandler.off(elem, 'mouseover', noop))
+ }
+
+ if (this._popper) {
+ this._popper.destroy()
+ }
+
+ this._menu.classList.remove(CLASS_NAME_SHOW)
+ this._element.classList.remove(CLASS_NAME_SHOW)
+ this._element.setAttribute('aria-expanded', 'false')
+ Manipulator.removeDataAttribute(this._menu, 'popper')
+ EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)
}
_getConfig(config) {
@@ -279,6 +237,35 @@ class Dropdown extends BaseComponent {
return config
}
+ _createPopper(parent) {
+ if (typeof Popper === 'undefined') {
+ throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)')
+ }
+
+ let referenceElement = this._element
+
+ if (this._config.reference === 'parent') {
+ referenceElement = parent
+ } else if (isElement(this._config.reference)) {
+ referenceElement = getElement(this._config.reference)
+ } else if (typeof this._config.reference === 'object') {
+ referenceElement = this._config.reference
+ }
+
+ const popperConfig = this._getPopperConfig()
+ const isDisplayStatic = popperConfig.modifiers.find(modifier => modifier.name === 'applyStyles' && modifier.enabled === false)
+
+ this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig)
+
+ if (isDisplayStatic) {
+ Manipulator.setDataAttribute(this._menu, 'popper', 'static')
+ }
+ }
+
+ _isShown(element = this._element) {
+ return element.classList.contains(CLASS_NAME_SHOW)
+ }
+
_getMenuElement() {
return SelectorEngine.next(this._element, SELECTOR_MENU)[0]
}
@@ -328,7 +315,6 @@ class Dropdown extends BaseComponent {
modifiers: [{
name: 'preventOverflow',
options: {
- altBoundary: this._config.flip,
boundary: this._config.boundary
}
},
@@ -354,28 +340,33 @@ class Dropdown extends BaseComponent {
}
}
+ _selectMenuItem({ key, target }) {
+ const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(isVisible)
+
+ if (!items.length) {
+ return
+ }
+
+ // if target isn't included in items (e.g. when expanding the dropdown)
+ // allow cycling to get the last item in case key equals ARROW_UP_KEY
+ getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus()
+ }
+
// Static
- static dropdownInterface(element, config) {
- let data = Data.getData(element, DATA_KEY)
- const _config = typeof config === 'object' ? config : null
+ static jQueryInterface(config) {
+ return this.each(function () {
+ const data = Dropdown.getOrCreateInstance(this, config)
- if (!data) {
- data = new Dropdown(element, _config)
- }
+ if (typeof config !== 'string') {
+ return
+ }
- if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
- }
- }
-
- static jQueryInterface(config) {
- return this.each(function () {
- Dropdown.dropdownInterface(this, config)
})
}
@@ -387,53 +378,41 @@ class Dropdown extends BaseComponent {
const toggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
for (let i = 0, len = toggles.length; i < len; i++) {
- const context = Data.getData(toggles[i], DATA_KEY)
- const relatedTarget = {
- relatedTarget: toggles[i]
- }
-
- if (event && event.type === 'click') {
- relatedTarget.clickEvent = event
- }
-
- if (!context) {
- continue
- }
-
- const dropdownMenu = context._menu
- if (!toggles[i].classList.contains(CLASS_NAME_SHOW)) {
+ const context = Dropdown.getInstance(toggles[i])
+ if (!context || context._config.autoClose === false) {
continue
}
- if (event && ((event.type === 'click' &&
- /input|textarea/i.test(event.target.tagName)) ||
- (event.type === 'keyup' && event.key === TAB_KEY)) &&
- dropdownMenu.contains(event.target)) {
+ if (!context._isShown()) {
continue
}
- const hideEvent = EventHandler.trigger(toggles[i], EVENT_HIDE, relatedTarget)
- if (hideEvent.defaultPrevented) {
- continue
+ const relatedTarget = {
+ relatedTarget: context._element
}
- // If this is a touch-enabled device we remove the extra
- // empty mouseover listeners we added for iOS support
- if ('ontouchstart' in document.documentElement) {
- [].concat(...document.body.children)
- .forEach(elem => EventHandler.off(elem, 'mouseover', null, noop()))
- }
+ if (event) {
+ const composedPath = event.composedPath()
+ const isMenuTarget = composedPath.includes(context._menu)
+ if (
+ composedPath.includes(context._element) ||
+ (context._config.autoClose === 'inside' && !isMenuTarget) ||
+ (context._config.autoClose === 'outside' && isMenuTarget)
+ ) {
+ continue
+ }
- toggles[i].setAttribute('aria-expanded', 'false')
+ // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu
+ if (context._menu.contains(event.target) && ((event.type === 'keyup' && event.key === TAB_KEY) || /input|select|option|textarea|form/i.test(event.target.tagName))) {
+ continue
+ }
- if (context._popper) {
- context._popper.destroy()
+ if (event.type === 'click') {
+ relatedTarget.clickEvent = event
+ }
}
- dropdownMenu.classList.remove(CLASS_NAME_SHOW)
- toggles[i].classList.remove(CLASS_NAME_SHOW)
- Manipulator.removeDataAttribute(dropdownMenu, 'popper')
- EventHandler.trigger(toggles[i], EVENT_HIDDEN, relatedTarget)
+ context._completeHide(relatedTarget)
}
}
@@ -457,56 +436,39 @@ class Dropdown extends BaseComponent {
return
}
- event.preventDefault()
- event.stopPropagation()
-
- if (this.disabled || this.classList.contains(CLASS_NAME_DISABLED)) {
- return
- }
-
- const parent = Dropdown.getParentFromElement(this)
const isActive = this.classList.contains(CLASS_NAME_SHOW)
- if (event.key === ESCAPE_KEY) {
- const button = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0]
- button.focus()
- Dropdown.clearMenus()
+ if (!isActive && event.key === ESCAPE_KEY) {
return
}
- if (!isActive && (event.key === ARROW_UP_KEY || event.key === ARROW_DOWN_KEY)) {
- const button = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0]
- button.click()
- return
- }
+ event.preventDefault()
+ event.stopPropagation()
- if (!isActive || event.key === SPACE_KEY) {
- Dropdown.clearMenus()
+ if (isDisabled(this)) {
return
}
- const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, parent).filter(isVisible)
+ const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0]
+ const instance = Dropdown.getOrCreateInstance(getToggleButton)
- if (!items.length) {
+ if (event.key === ESCAPE_KEY) {
+ instance.hide()
return
}
- let index = items.indexOf(event.target)
+ if (event.key === ARROW_UP_KEY || event.key === ARROW_DOWN_KEY) {
+ if (!isActive) {
+ instance.show()
+ }
- // Up
- if (event.key === ARROW_UP_KEY && index > 0) {
- index--
+ instance._selectMenuItem(event)
+ return
}
- // Down
- if (event.key === ARROW_DOWN_KEY && index < items.length - 1) {
- index++
+ if (!isActive || event.key === SPACE_KEY) {
+ Dropdown.clearMenus()
}
-
- // index is -1 if the first keydown is an ArrowUp
- index = index === -1 ? 0 : index
-
- items[index].focus()
}
}
@@ -522,10 +484,8 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus)
EventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus)
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
event.preventDefault()
- event.stopPropagation()
- Dropdown.dropdownInterface(this, 'toggle')
+ Dropdown.getOrCreateInstance(this).toggle()
})
-EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_FORM_CHILD, e => e.stopPropagation())
/**
* ------------------------------------------------------------------------
@@ -534,6 +494,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_FORM_CHILD, e => e.stop
* add .Dropdown to jQuery only if jQuery is present
*/
-defineJQueryPlugin(NAME, Dropdown)
+defineJQueryPlugin(Dropdown)
export default Dropdown