@@ -2,8 +2,11 @@ import Popper from 'popper.js'
22import KeyCodes from '../utils/key-codes'
33import warn from '../utils/warn'
44import { BvEvent } from '../utils/bv-event.class'
5- import { closest , contains , isVisible , requestAF , selectAll , eventOn , eventOff } from '../utils/dom'
5+ import { closest , contains , isVisible , requestAF , selectAll } from '../utils/dom'
6+ import { hasTouchSupport } from '../utils/env'
67import { isNull } from '../utils/inspect'
8+ import clickOutMixin from './click-out'
9+ import focusInMixin from './focus-in'
710import idMixin from './id'
811
912// Return an array of visible items
@@ -15,7 +18,7 @@ const ROOT_DROPDOWN_SHOWN = `${ROOT_DROPDOWN_PREFIX}shown`
1518const ROOT_DROPDOWN_HIDDEN = `${ ROOT_DROPDOWN_PREFIX } hidden`
1619
1720// Delay when loosing focus before closing menu (in ms)
18- const FOCUSOUT_DELAY = 100
21+ const FOCUSOUT_DELAY = hasTouchSupport ? 450 : 150
1922
2023// Dropdown item CSS selectors
2124const Selector = {
@@ -47,7 +50,7 @@ const AttachmentMap = {
4750
4851// @vue /component
4952export default {
50- mixins : [ idMixin ] ,
53+ mixins : [ idMixin , clickOutMixin , focusInMixin ] ,
5154 provide ( ) {
5255 return {
5356 bvDropdown : this
@@ -171,18 +174,21 @@ export default {
171174 } ,
172175 created ( ) {
173176 // Create non-reactive property
174- this . _popper = null
177+ this . $_popper = null
178+ this . $_hideTimeout = null
179+ this . $_noop = ( ) => { }
175180 } ,
176181 deactivated ( ) /* istanbul ignore next: not easy to test */ {
177182 // In case we are inside a `<keep-alive>`
178183 this . visible = false
179184 this . whileOpenListen ( false )
180- this . removePopper ( )
185+ this . destroyPopper ( )
181186 } ,
182187 beforeDestroy ( ) {
183188 this . visible = false
184189 this . whileOpenListen ( false )
185- this . removePopper ( )
190+ this . destroyPopper ( )
191+ this . clearHideTimeout ( )
186192 } ,
187193 methods : {
188194 // Event emitter
@@ -235,18 +241,25 @@ export default {
235241 this . whileOpenListen ( false )
236242 this . $root . $emit ( ROOT_DROPDOWN_HIDDEN , this )
237243 this . $emit ( 'hidden' )
238- this . removePopper ( )
244+ this . destroyPopper ( )
239245 } ,
240246 createPopper ( element ) {
241- this . removePopper ( )
242- this . _popper = new Popper ( element , this . $refs . menu , this . getPopperConfig ( ) )
247+ this . destroyPopper ( )
248+ this . $ _popper = new Popper ( element , this . $refs . menu , this . getPopperConfig ( ) )
243249 } ,
244- removePopper ( ) {
245- if ( this . _popper ) {
250+ destroyPopper ( ) {
251+ if ( this . $ _popper) {
246252 // Ensure popper event listeners are removed cleanly
247- this . _popper . destroy ( )
253+ this . $_popper . destroy ( )
254+ }
255+ this . $_popper = null
256+ } ,
257+ clearHideTimeout ( ) {
258+ /* istanbul ignore next */
259+ if ( this . $_hideTimeout ) {
260+ clearTimeout ( this . $_hideTimeout )
261+ this . $_hideTimeout = null
248262 }
249- this . _popper = null
250263 } ,
251264 getPopperConfig ( ) {
252265 let placement = AttachmentMap . BOTTOM
@@ -271,17 +284,15 @@ export default {
271284 }
272285 return { ...popperConfig , ...( this . popperOpts || { } ) }
273286 } ,
287+ // Turn listeners on/off while open
274288 whileOpenListen ( isOpen ) {
275- // turn listeners on/off while open
276- if ( isOpen ) {
277- // If another dropdown is opened
278- this . $root . $on ( ROOT_DROPDOWN_SHOWN , this . rootCloseListener )
279- // Hide the menu when focus moves out
280- eventOn ( this . $el , 'focusout' , this . onFocusOut , { passive : true } )
281- } else {
282- this . $root . $off ( ROOT_DROPDOWN_SHOWN , this . rootCloseListener )
283- eventOff ( this . $el , 'focusout' , this . onFocusOut , { passive : true } )
284- }
289+ // Hide the dropdown when clicked outside
290+ this . listenForClickOut = isOpen
291+ // Hide the dropdown when it loses focus
292+ this . listenForFocusIn = isOpen
293+ // Hide the dropdown when another dropdown is opened
294+ const method = isOpen ? '$on' : '$off'
295+ this . $root [ method ] ( ROOT_DROPDOWN_SHOWN , this . rootCloseListener )
285296 } ,
286297 rootCloseListener ( vm ) {
287298 if ( vm !== this ) {
@@ -375,27 +386,28 @@ export default {
375386 this . $once ( 'hidden' , this . focusToggler )
376387 }
377388 } ,
378- // Dropdown wrapper focusOut handler
379- onFocusOut ( evt ) {
380- // `relatedTarget` is the element gaining focus
381- const relatedTarget = evt . relatedTarget
382- // If focus moves outside the menu or toggler, then close menu
383- if (
384- this . visible &&
385- ! contains ( this . $refs . menu , relatedTarget ) &&
386- ! contains ( this . toggler , relatedTarget )
387- ) {
389+ // Document click out listener
390+ clickOutHandler ( evt ) {
391+ const target = evt . target
392+ if ( this . visible && ! contains ( this . $refs . menu , target ) && ! contains ( this . toggler , target ) ) {
388393 const doHide = ( ) => {
389394 this . visible = false
395+ return null
390396 }
391397 // When we are in a navbar (which has been responsively stacked), we
392398 // delay the dropdown's closing so that the next element has a chance
393399 // to have it's click handler fired (in case it's position moves on
394400 // the screen do to a navbar menu above it collapsing)
395401 // https://github.com/bootstrap-vue/bootstrap-vue/issues/4113
396- this . inNavbar ? setTimeout ( doHide , FOCUSOUT_DELAY ) : doHide ( )
402+ this . clearHideTimeout ( )
403+ this . $_hideTimeout = this . inNavbar ? setTimeout ( doHide , FOCUSOUT_DELAY ) : doHide ( )
397404 }
398405 } ,
406+ // Document focusin listener
407+ focusInHandler ( evt ) {
408+ // Shared logic with click-out handler
409+ this . clickOutHandler ( evt )
410+ } ,
399411 // Keyboard nav
400412 focusNext ( evt , up ) {
401413 // Ignore key up/down on form elements
0 commit comments