如何用Vue实现Bootstrap Dropdown的自动关闭行为?
移除Bootstrap 5 JS依赖后Dropdown自动关闭的问题
我尝试移除Bootstrap 5的JavaScript依赖(保留CSS样式),用Vue管理所有状态。Modal处理很顺利,但Dropdown组件遇到了问题。
当前实现代码
page.js
// And yes, I am using Global Build of Vue, for some reason const vm = Vue.createApp({ data() { return { ... } }, ... }).component('base-dropdown', { data() { return { show: false } }, props: { variant: { default: 'btn-outline-secondary', type: String }, text: String, placement: String, autoClose: { default: true, validator(value) { return [true, false, 'inside', 'outside'].indexOf(value) > -1 } } }, methods: { close(ev) { if (!this.show || !this.autoClose) { return } const rect = this.$refs.menu.getBoundingClientRect() if (this.autoClose === 'inside') { if (ev.clientX >= rect.left && ev.clientX <= rect.right && ev.clientY >= rect.top && ev.clientY <= rect.bottom) { this.show = false return } } else if (this.autoClose === 'outside') { if (ev.clientX < rect.left || ev.clientX > rect.right || ev.clientY < rect.top && ev.clientY > rect.bottom) { this.show = false return } } else { this.show = false } }, }, watch: { show(val) { if (val) { setTimeout(() => window.addEventListener('click', this.close)) } else { window.removeEventListener('click', this.close) } }, }, template: '#base-dropdown', }).mount('#app')
HTML文件
<!doctype html> <html> <head> ... <script src="/url/to/cdn/vue.global.js"></script> </head> <body> <div id="app"> ... </div> <template id="base-dropdown"> <button type="button" class="btn dropdown-toggle" :class="[variant,text||'dropdown-toggle-split',show?'show':'']" @click="show=!show" >{{ text }}</button> <ul ref="menu" class="dropdown-menu" :class="[placement,show?'show':'']" :data-bs-popper="show?'static':false" ><slot></slot></ul> </template> <script src="/path/to/page.js"></script> </body> </html>
问题详情
当autoClose设置为inside(仅点击菜单内部或切换按钮时关闭下拉),如果菜单被滚动到某个header后方,点击该header区域时菜单会意外关闭。我知道这是因为用了指针位置和菜单的bounding rect比较来判断是否关闭,但原生Bootstrap Dropdown没有这个问题。想知道Bootstrap是怎么实现自动关闭行为的?它也是给window加点击事件做条件判断吗?
解决方案与Bootstrap原生实现解析
Bootstrap 5的Dropdown自动关闭逻辑确实是通过监听window的点击事件实现,但它的判断逻辑并非基于元素的坐标位置,而是通过事件目标的DOM归属关系来判断:
核心判断逻辑:Bootstrap会在点击事件触发时,用
Element.closest()方法检查事件target是否属于Dropdown的内部DOM树(包括触发按钮、菜单容器及所有子元素),而非依赖getBoundingClientRect()的坐标计算。autoClose分支处理:
autoClose: 'inside':仅当点击Dropdown内部元素时关闭菜单,外部点击不触发关闭autoClose: 'outside':仅当点击Dropdown外部元素时关闭菜单,内部点击不触发关闭- 默认
true:点击任何区域都会关闭菜单
修复你的实现:
把基于坐标的判断改为基于DOM归属的判断,即可解决滚动后坐标错位的问题:close(ev) { if (!this.show || !this.autoClose) { return } // 获取Dropdown根容器(需将按钮和菜单包裹在同一容器内) const dropdownRoot = this.$el.closest('.dropdown'); const isClickInside = dropdownRoot.contains(ev.target); if (this.autoClose === 'inside') { if (isClickInside) { this.show = false; } } else if (this.autoClose === 'outside') { if (!isClickInside) { this.show = false; } } else { this.show = false; } }同时调整模板,将按钮和菜单放在同一个带
dropdown类的容器中:<template id="base-dropdown"> <div class="dropdown"> <button type="button" class="btn dropdown-toggle" :class="[variant, text ? '' : 'dropdown-toggle-split', show?'show':'']" @click="show=!show" >{{ text }}</button> <ul ref="menu" class="dropdown-menu" :class="[placement, show?'show':'']" :data-bs-popper="show?'static':false" ><slot></slot></ul> </div> </template>这样无论菜单是否被其他元素遮挡,只要点击的元素不属于Dropdown容器内部,就不会触发
inside模式下的关闭。
内容的提问来源于stack exchange,提问作者Ezon Zhao
相关产品推荐
相关产品推荐

