blob: 3689b3fdddf3d2f9a1d123ccef9d924b2cdb21b2 [file] [log] [blame] [edit]
/**
* --------------------------------------------------------------------------
* Bootstrap menu.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import {
computePosition,
flip,
shift,
offset,
autoUpdate
} from '@floating-ui/dom'
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import Manipulator from './dom/manipulator.js'
import SelectorEngine from './dom/selector-engine.js'
import {
execute,
getElement,
getNextActiveElement,
isDisabled,
isElement,
isRTL,
isVisible,
noop
} from './util/index.js'
import {
parseResponsivePlacement,
getResponsivePlacement,
createBreakpointListeners,
disposeBreakpointListeners
} from './util/floating-ui.js'
/**
* Constants
*/
const NAME = 'menu'
const DATA_KEY = 'bs.menu'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const ESCAPE_KEY = 'Escape'
const TAB_KEY = 'Tab'
const ARROW_UP_KEY = 'ArrowUp'
const ARROW_DOWN_KEY = 'ArrowDown'
const ARROW_LEFT_KEY = 'ArrowLeft'
const ARROW_RIGHT_KEY = 'ArrowRight'
const HOME_KEY = 'Home'
const END_KEY = 'End'
const ENTER_KEY = 'Enter'
const SPACE_KEY = ' '
const RIGHT_MOUSE_BUTTON = 2
const SUBMENU_CLOSE_DELAY = 100
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_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_SHOW = 'show'
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="menu"]:not(.disabled):not(:disabled)'
const SELECTOR_MENU = '.menu'
const SELECTOR_SUBMENU = '.submenu'
const SELECTOR_SUBMENU_TOGGLE = '.submenu > .menu-item'
const SELECTOR_NAVBAR_NAV = '.navbar-nav'
const SELECTOR_VISIBLE_ITEMS = '.menu-item:not(.disabled):not(:disabled)'
const DEFAULT_PLACEMENT = 'bottom-start'
const SUBMENU_PLACEMENT = 'end-start'
const resolveLogicalPlacement = placement => {
if (isRTL()) {
return placement.replace(/^start(?=-|$)/, 'right').replace(/^end(?=-|$)/, 'left')
}
return placement.replace(/^start(?=-|$)/, 'left').replace(/^end(?=-|$)/, 'right')
}
const triangleSign = (p1, p2, p3) =>
((p1.x - p3.x) * (p2.y - p3.y)) - ((p2.x - p3.x) * (p1.y - p3.y))
const Default = {
autoClose: true,
boundary: 'clippingParents',
container: false,
display: 'dynamic',
offset: [0, 2],
floatingConfig: null,
menu: null,
placement: DEFAULT_PLACEMENT,
reference: 'toggle',
strategy: 'absolute',
submenuTrigger: 'both',
submenuDelay: SUBMENU_CLOSE_DELAY
}
const DefaultType = {
autoClose: '(boolean|string)',
boundary: '(string|element)',
container: '(string|element|boolean)',
display: 'string',
offset: '(array|string|function)',
floatingConfig: '(null|object|function)',
menu: '(null|element)',
placement: 'string',
reference: '(string|element|object)',
strategy: 'string',
submenuTrigger: 'string',
submenuDelay: 'number'
}
/**
* Class definition
*/
class Menu extends BaseComponent {
static _openInstances = new Set()
constructor(element, config) {
if (typeof computePosition === 'undefined') {
throw new TypeError('Bootstrap\'s menus require Floating UI (https://floating-ui.com)')
}
super(element, config)
this._floatingCleanup = null
this._mediaQueryListeners = []
this._responsivePlacements = null
this._parent = this._element.parentNode
this._isSubmenu = this._parent.classList?.contains('submenu')
this._openSubmenus = new Map()
this._submenuCloseTimeouts = new Map()
this._hoverIntentData = null
this._menu = this._config.menu || this._findMenu()
this._menuOriginalParent = this._menu?.parentNode
this._parseResponsivePlacements()
this._setupSubmenuListeners()
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
toggle() {
return this._isShown() ? this.hide() : this.show()
}
show() {
if (isDisabled(this._element) || this._isShown()) {
return
}
const relatedTarget = {
relatedTarget: this._element
}
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget)
if (showEvent.defaultPrevented) {
return
}
this._moveMenuToContainer()
this._createFloating()
if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {
for (const element of [].concat(...document.body.children)) {
EventHandler.on(element, 'mouseover', noop)
}
}
this._element.focus({ focusVisible: false })
this._element.setAttribute('aria-expanded', 'true')
this._menu.classList.add(CLASS_NAME_SHOW)
this._element.classList.add(CLASS_NAME_SHOW)
if (this._parent) {
this._parent.classList.add(CLASS_NAME_SHOW)
}
Menu._openInstances.add(this)
EventHandler.trigger(this._element, EVENT_SHOWN, relatedTarget)
}
hide() {
if (isDisabled(this._element) || !this._isShown()) {
return
}
const relatedTarget = {
relatedTarget: this._element
}
this._completeHide(relatedTarget)
}
dispose() {
this._disposeFloating()
this._restoreMenuToOriginalParent()
this._disposeMediaQueryListeners()
this._closeAllSubmenus()
this._clearAllSubmenuTimeouts()
Menu._openInstances.delete(this)
super.dispose()
}
update() {
if (this._floatingCleanup) {
this._updateFloatingPosition()
}
}
// Private
_findMenu() {
return SelectorEngine.next(this._element, SELECTOR_MENU)[0] ||
SelectorEngine.prev(this._element, SELECTOR_MENU)[0] ||
SelectorEngine.findOne(SELECTOR_MENU, this._parent)
}
_completeHide(relatedTarget) {
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget)
if (hideEvent.defaultPrevented) {
return
}
this._closeAllSubmenus()
if ('ontouchstart' in document.documentElement) {
for (const element of [].concat(...document.body.children)) {
EventHandler.off(element, 'mouseover', noop)
}
}
this._disposeFloating()
this._restoreMenuToOriginalParent()
this._menu.classList.remove(CLASS_NAME_SHOW)
this._element.classList.remove(CLASS_NAME_SHOW)
if (this._parent) {
this._parent.classList.remove(CLASS_NAME_SHOW)
}
this._element.setAttribute('aria-expanded', 'false')
Manipulator.removeDataAttribute(this._menu, 'placement')
Manipulator.removeDataAttribute(this._menu, 'display')
Menu._openInstances.delete(this)
EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)
}
_getConfig(config) {
config = super._getConfig(config)
if (typeof config.reference === 'object' && !isElement(config.reference) &&
typeof config.reference.getBoundingClientRect !== 'function'
) {
throw new TypeError(`${NAME.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`)
}
return config
}
_createFloating() {
if (this._config.display === 'static') {
Manipulator.setDataAttribute(this._menu, 'display', 'static')
return
}
let referenceElement = this._element
if (this._config.reference === 'parent') {
referenceElement = this._parent
} else if (isElement(this._config.reference)) {
referenceElement = getElement(this._config.reference)
} else if (typeof this._config.reference === 'object') {
referenceElement = this._config.reference
}
this._updateFloatingPosition(referenceElement)
this._floatingCleanup = autoUpdate(
referenceElement,
this._menu,
() => this._updateFloatingPosition(referenceElement)
)
}
async _updateFloatingPosition(referenceElement = null) {
if (!this._menu) {
return
}
if (!referenceElement) {
if (this._config.reference === 'parent') {
referenceElement = this._parent
} else if (isElement(this._config.reference)) {
referenceElement = getElement(this._config.reference)
} else if (typeof this._config.reference === 'object') {
referenceElement = this._config.reference
} else {
referenceElement = this._element
}
}
const placement = this._getPlacement()
const middleware = this._getFloatingMiddleware()
const floatingConfig = this._getFloatingConfig(placement, middleware)
await this._applyFloatingPosition(
referenceElement,
this._menu,
floatingConfig.placement,
floatingConfig.middleware,
floatingConfig.strategy
)
}
_isShown() {
return this._menu.classList.contains(CLASS_NAME_SHOW)
}
_getPlacement() {
const placement = this._responsivePlacements ?
getResponsivePlacement(this._responsivePlacements, DEFAULT_PLACEMENT) :
this._config.placement
return resolveLogicalPlacement(placement)
}
_parseResponsivePlacements() {
this._responsivePlacements = parseResponsivePlacement(this._config.placement, DEFAULT_PLACEMENT)
if (this._responsivePlacements) {
this._setupMediaQueryListeners()
}
}
_setupMediaQueryListeners() {
this._disposeMediaQueryListeners()
this._mediaQueryListeners = createBreakpointListeners(() => {
if (this._isShown()) {
this._updateFloatingPosition()
}
})
}
_disposeMediaQueryListeners() {
disposeBreakpointListeners(this._mediaQueryListeners)
this._mediaQueryListeners = []
}
_getOffset() {
const { offset: offsetConfig } = this._config
if (typeof offsetConfig === 'string') {
return offsetConfig.split(',').map(value => Number.parseInt(value, 10))
}
if (typeof offsetConfig === 'function') {
return ({ placement, rects }) => {
const result = offsetConfig({ placement, reference: rects.reference, floating: rects.floating }, this._element)
return result
}
}
return offsetConfig
}
_getFloatingMiddleware() {
const offsetValue = this._getOffset()
const middleware = [
offset(
typeof offsetValue === 'function' ?
offsetValue :
{ mainAxis: offsetValue[1] || 0, crossAxis: offsetValue[0] || 0 }
),
flip({
fallbackPlacements: this._getFallbackPlacements()
}),
shift({
boundary: this._config.boundary === 'clippingParents' ? 'clippingAncestors' : this._config.boundary
})
]
return middleware
}
_getFallbackPlacements() {
const placement = this._getPlacement()
const fallbackMap = {
bottom: ['top', 'bottom-start', 'bottom-end', 'top-start', 'top-end'],
'bottom-start': ['top-start', 'bottom-end', 'top-end'],
'bottom-end': ['top-end', 'bottom-start', 'top-start'],
top: ['bottom', 'top-start', 'top-end', 'bottom-start', 'bottom-end'],
'top-start': ['bottom-start', 'top-end', 'bottom-end'],
'top-end': ['bottom-end', 'top-start', 'bottom-start'],
right: ['left', 'right-start', 'right-end', 'left-start', 'left-end'],
'right-start': ['left-start', 'right-end', 'left-end', 'top-start', 'bottom-start'],
'right-end': ['left-end', 'right-start', 'left-start', 'top-end', 'bottom-end'],
left: ['right', 'left-start', 'left-end', 'right-start', 'right-end'],
'left-start': ['right-start', 'left-end', 'right-end', 'top-start', 'bottom-start'],
'left-end': ['right-end', 'left-start', 'right-start', 'top-end', 'bottom-end']
}
return fallbackMap[placement] || ['top', 'bottom', 'right', 'left']
}
_getFloatingConfig(placement, middleware) {
const defaultConfig = {
placement,
middleware,
strategy: this._config.strategy
}
return {
...defaultConfig,
...execute(this._config.floatingConfig, [undefined, defaultConfig])
}
}
_disposeFloating() {
if (this._floatingCleanup) {
this._floatingCleanup()
this._floatingCleanup = null
}
}
_getContainer() {
const { container } = this._config
if (container === false) {
return null
}
return container === true ? document.body : getElement(container)
}
_moveMenuToContainer() {
const container = this._getContainer()
if (!container || !this._menu) {
return
}
if (this._menu.parentNode !== container) {
container.append(this._menu)
}
}
_restoreMenuToOriginalParent() {
if (!this._menuOriginalParent || !this._menu) {
return
}
if (this._menu.parentNode !== this._menuOriginalParent) {
this._menuOriginalParent.append(this._menu)
}
}
async _applyFloatingPosition(reference, floating, placement, middleware, strategy = 'absolute') {
if (!floating.isConnected) {
return null
}
const { x, y, placement: finalPlacement } = await computePosition(
reference,
floating,
{ placement, middleware, strategy }
)
if (!floating.isConnected) {
return null
}
Object.assign(floating.style, {
position: strategy,
left: `${x}px`,
top: `${y}px`,
margin: '0'
})
Manipulator.setDataAttribute(floating, 'placement', finalPlacement)
return finalPlacement
}
// -------------------------------------------------------------------------
// Submenu handling
// -------------------------------------------------------------------------
_setupSubmenuListeners() {
if (this._config.submenuTrigger === 'hover' || this._config.submenuTrigger === 'both') {
EventHandler.on(this._menu, 'mouseenter', SELECTOR_SUBMENU_TOGGLE, event => {
this._onSubmenuTriggerEnter(event)
})
EventHandler.on(this._menu, 'mouseleave', SELECTOR_SUBMENU, event => {
this._onSubmenuLeave(event)
})
EventHandler.on(this._menu, 'mousemove', event => {
this._trackMousePosition(event)
})
}
if (this._config.submenuTrigger === 'click' || this._config.submenuTrigger === 'both') {
EventHandler.on(this._menu, 'click', SELECTOR_SUBMENU_TOGGLE, event => {
this._onSubmenuTriggerClick(event)
})
}
}
_onSubmenuTriggerEnter(event) {
const trigger = event.target.closest(SELECTOR_SUBMENU_TOGGLE)
if (!trigger) {
return
}
const submenuWrapper = trigger.closest(SELECTOR_SUBMENU)
const submenu = SelectorEngine.findOne(SELECTOR_MENU, submenuWrapper)
if (!submenu) {
return
}
this._cancelSubmenuCloseTimeout(submenu)
this._closeSiblingSubmenus(submenuWrapper)
this._openSubmenu(trigger, submenu, submenuWrapper)
}
_onSubmenuLeave(event) {
const submenuWrapper = event.target.closest(SELECTOR_SUBMENU)
const submenu = SelectorEngine.findOne(SELECTOR_MENU, submenuWrapper)
if (!submenu || !this._openSubmenus.has(submenu)) {
return
}
if (this._isMovingTowardSubmenu(event, submenu)) {
return
}
this._scheduleSubmenuClose(submenu, submenuWrapper)
}
_onSubmenuTriggerClick(event) {
const trigger = event.target.closest(SELECTOR_SUBMENU_TOGGLE)
if (!trigger) {
return
}
event.preventDefault()
event.stopPropagation()
const submenuWrapper = trigger.closest(SELECTOR_SUBMENU)
const submenu = SelectorEngine.findOne(SELECTOR_MENU, submenuWrapper)
if (!submenu) {
return
}
if (this._openSubmenus.has(submenu)) {
this._closeSubmenu(submenu, submenuWrapper)
} else {
this._closeSiblingSubmenus(submenuWrapper)
this._openSubmenu(trigger, submenu, submenuWrapper)
}
}
_openSubmenu(trigger, submenu, submenuWrapper) {
if (this._openSubmenus.has(submenu)) {
return
}
trigger.setAttribute('aria-expanded', 'true')
trigger.setAttribute('aria-haspopup', 'true')
submenu.classList.add(CLASS_NAME_SHOW)
submenuWrapper.classList.add(CLASS_NAME_SHOW)
const cleanup = this._createSubmenuFloating(trigger, submenu, submenuWrapper)
this._openSubmenus.set(submenu, cleanup)
EventHandler.on(submenu, 'mouseenter', () => {
this._cancelSubmenuCloseTimeout(submenu)
})
}
_closeSubmenu(submenu, submenuWrapper) {
if (!this._openSubmenus.has(submenu)) {
return
}
const nestedSubmenus = SelectorEngine.find(`${SELECTOR_SUBMENU} ${SELECTOR_MENU}.${CLASS_NAME_SHOW}`, submenu)
for (const nested of nestedSubmenus) {
const nestedWrapper = nested.closest(SELECTOR_SUBMENU)
this._closeSubmenu(nested, nestedWrapper)
}
const trigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, submenuWrapper)
const cleanup = this._openSubmenus.get(submenu)
if (cleanup) {
cleanup()
}
this._openSubmenus.delete(submenu)
EventHandler.off(submenu, 'mouseenter')
if (trigger) {
trigger.setAttribute('aria-expanded', 'false')
}
submenu.classList.remove(CLASS_NAME_SHOW)
submenuWrapper.classList.remove(CLASS_NAME_SHOW)
submenu.style.position = ''
submenu.style.left = ''
submenu.style.top = ''
submenu.style.margin = ''
}
_closeAllSubmenus() {
for (const [submenu] of this._openSubmenus) {
const submenuWrapper = submenu.closest(SELECTOR_SUBMENU)
this._closeSubmenu(submenu, submenuWrapper)
}
}
_closeSiblingSubmenus(currentSubmenuWrapper) {
const parent = currentSubmenuWrapper.parentNode
const siblingSubmenus = SelectorEngine.find(`${SELECTOR_SUBMENU} > ${SELECTOR_MENU}.${CLASS_NAME_SHOW}`, parent)
for (const siblingMenu of siblingSubmenus) {
const siblingWrapper = siblingMenu.closest(SELECTOR_SUBMENU)
if (siblingWrapper !== currentSubmenuWrapper) {
this._closeSubmenu(siblingMenu, siblingWrapper)
}
}
}
_createSubmenuFloating(trigger, submenu, submenuWrapper) {
const referenceElement = submenuWrapper
const placement = resolveLogicalPlacement(SUBMENU_PLACEMENT)
const middleware = [
offset({ mainAxis: 0, crossAxis: -4 }),
flip({
fallbackPlacements: [
resolveLogicalPlacement('start-start'),
resolveLogicalPlacement('end-end'),
resolveLogicalPlacement('start-end')
]
}),
shift({ padding: 8 })
]
const updatePosition = () => this._applyFloatingPosition(referenceElement, submenu, placement, middleware)
updatePosition()
return autoUpdate(referenceElement, submenu, updatePosition)
}
_scheduleSubmenuClose(submenu, submenuWrapper) {
this._cancelSubmenuCloseTimeout(submenu)
const timeoutId = setTimeout(() => {
this._closeSubmenu(submenu, submenuWrapper)
this._submenuCloseTimeouts.delete(submenu)
}, this._config.submenuDelay)
this._submenuCloseTimeouts.set(submenu, timeoutId)
}
_cancelSubmenuCloseTimeout(submenu) {
const timeoutId = this._submenuCloseTimeouts.get(submenu)
if (timeoutId) {
clearTimeout(timeoutId)
this._submenuCloseTimeouts.delete(submenu)
}
}
_clearAllSubmenuTimeouts() {
for (const timeoutId of this._submenuCloseTimeouts.values()) {
clearTimeout(timeoutId)
}
this._submenuCloseTimeouts.clear()
}
// -------------------------------------------------------------------------
// Hover intent / Safe triangle
// -------------------------------------------------------------------------
_trackMousePosition(event) {
this._hoverIntentData = {
x: event.clientX,
y: event.clientY,
timestamp: Date.now()
}
}
_isMovingTowardSubmenu(event, submenu) {
if (!this._hoverIntentData) {
return false
}
const submenuRect = submenu.getBoundingClientRect()
const currentPos = { x: event.clientX, y: event.clientY }
const lastPos = { x: this._hoverIntentData.x, y: this._hoverIntentData.y }
const isRtl = isRTL()
const targetX = isRtl ? submenuRect.right : submenuRect.left
const topCorner = { x: targetX, y: submenuRect.top }
const bottomCorner = { x: targetX, y: submenuRect.bottom }
return this._pointInTriangle(currentPos, lastPos, topCorner, bottomCorner)
}
_pointInTriangle(point, v1, v2, v3) {
const d1 = triangleSign(point, v1, v2)
const d2 = triangleSign(point, v2, v3)
const d3 = triangleSign(point, v3, v1)
const hasNeg = (d1 < 0) || (d2 < 0) || (d3 < 0)
const hasPos = (d1 > 0) || (d2 > 0) || (d3 > 0)
return !(hasNeg && hasPos)
}
// -------------------------------------------------------------------------
// Keyboard navigation
// -------------------------------------------------------------------------
_selectMenuItem({ key, target }) {
const currentMenu = target.closest(SELECTOR_MENU) || this._menu
const items = SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS}`, currentMenu)
.filter(element => isVisible(element))
if (!items.length) {
return
}
getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus()
}
_handleSubmenuKeydown(event) {
const { key, target } = event
const isRtl = isRTL()
const enterKey = isRtl ? ARROW_LEFT_KEY : ARROW_RIGHT_KEY
const exitKey = isRtl ? ARROW_RIGHT_KEY : ARROW_LEFT_KEY
const submenuWrapper = target.closest(SELECTOR_SUBMENU)
const isSubmenuTrigger = submenuWrapper && target.matches(SELECTOR_SUBMENU_TOGGLE)
if ((key === ENTER_KEY || key === SPACE_KEY) && isSubmenuTrigger) {
event.preventDefault()
event.stopPropagation()
const submenu = SelectorEngine.findOne(SELECTOR_MENU, submenuWrapper)
if (submenu) {
this._closeSiblingSubmenus(submenuWrapper)
this._openSubmenu(target, submenu, submenuWrapper)
requestAnimationFrame(() => {
const firstItem = SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS, submenu)
if (firstItem) {
firstItem.focus()
}
})
}
return true
}
if (key === enterKey && isSubmenuTrigger) {
event.preventDefault()
event.stopPropagation()
const submenu = SelectorEngine.findOne(SELECTOR_MENU, submenuWrapper)
if (submenu) {
this._closeSiblingSubmenus(submenuWrapper)
this._openSubmenu(target, submenu, submenuWrapper)
requestAnimationFrame(() => {
const firstItem = SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS, submenu)
if (firstItem) {
firstItem.focus()
}
})
}
return true
}
if (key === exitKey) {
const currentMenu = target.closest(SELECTOR_MENU)
const parentSubmenuWrapper = currentMenu?.closest(SELECTOR_SUBMENU)
if (parentSubmenuWrapper) {
event.preventDefault()
event.stopPropagation()
const parentTrigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, parentSubmenuWrapper)
this._closeSubmenu(currentMenu, parentSubmenuWrapper)
if (parentTrigger) {
parentTrigger.focus()
}
return true
}
}
if (key === HOME_KEY || key === END_KEY) {
event.preventDefault()
event.stopPropagation()
const currentMenu = target.closest(SELECTOR_MENU)
const items = SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS}`, currentMenu)
.filter(element => isVisible(element))
if (items.length) {
const targetItem = key === HOME_KEY ? items[0] : items[items.length - 1]
targetItem.focus()
}
return true
}
return false
}
static clearMenus(event) {
if (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY)) {
return
}
for (const instance of Menu._openInstances) {
if (instance._config.autoClose === false) {
continue
}
const composedPath = event.composedPath()
const isMenuTarget = composedPath.includes(instance._menu)
if (
composedPath.includes(instance._element) ||
(instance._config.autoClose === 'inside' && !isMenuTarget) ||
(instance._config.autoClose === 'outside' && isMenuTarget)
) {
continue
}
if (instance._menu.contains(event.target) && ((event.type === 'keyup' && event.key === TAB_KEY) || /input|select|option|textarea|form/i.test(event.target.tagName))) {
continue
}
const relatedTarget = { relatedTarget: instance._element }
if (event.type === 'click') {
relatedTarget.clickEvent = event
}
instance._completeHide(relatedTarget)
}
}
static dataApiKeydownHandler(event) {
const isInput = /input|textarea/i.test(event.target.tagName)
const isEscapeEvent = event.key === ESCAPE_KEY
const isUpOrDownEvent = [ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key)
const isLeftOrRightEvent = [ARROW_LEFT_KEY, ARROW_RIGHT_KEY].includes(event.key)
const isHomeOrEndEvent = [HOME_KEY, END_KEY].includes(event.key)
const isEnterOrSpaceEvent = [ENTER_KEY, SPACE_KEY].includes(event.key)
const isSubmenuTrigger = event.target.matches(SELECTOR_SUBMENU_TOGGLE)
if (!isUpOrDownEvent && !isEscapeEvent && !isLeftOrRightEvent && !isHomeOrEndEvent &&
!(isEnterOrSpaceEvent && isSubmenuTrigger)) {
return
}
if (isInput && !isEscapeEvent) {
return
}
const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ?
this :
(SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] ||
SelectorEngine.next(this, SELECTOR_DATA_TOGGLE)[0] ||
SelectorEngine.findOne(SELECTOR_DATA_TOGGLE, event.delegateTarget.parentNode))
if (!getToggleButton) {
return
}
const instance = Menu.getOrCreateInstance(getToggleButton)
if ((isLeftOrRightEvent || isHomeOrEndEvent || (isEnterOrSpaceEvent && isSubmenuTrigger)) && instance._handleSubmenuKeydown(event)) {
return
}
if (isUpOrDownEvent) {
event.preventDefault()
event.stopPropagation()
instance.show()
instance._selectMenuItem(event)
return
}
if (isEscapeEvent && instance._isShown()) {
event.preventDefault()
event.stopPropagation()
const currentMenu = event.target.closest(SELECTOR_MENU)
const parentSubmenuWrapper = currentMenu?.closest(SELECTOR_SUBMENU)
if (parentSubmenuWrapper && instance._openSubmenus.size > 0) {
const parentTrigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, parentSubmenuWrapper)
instance._closeSubmenu(currentMenu, parentSubmenuWrapper)
if (parentTrigger) {
parentTrigger.focus()
}
return
}
instance.hide()
getToggleButton.focus()
}
}
}
/**
* Data API implementation
*/
EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Menu.dataApiKeydownHandler)
EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Menu.dataApiKeydownHandler)
EventHandler.on(document, EVENT_CLICK_DATA_API, Menu.clearMenus)
EventHandler.on(document, EVENT_KEYUP_DATA_API, Menu.clearMenus)
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
event.preventDefault()
Menu.getOrCreateInstance(this).toggle()
})
export default Menu