1
1
import Popper from 'popper.js'
2
- import { BvEvent } from '../utils/bv-event.class'
3
2
import KeyCodes from '../utils/key-codes'
4
3
import warn from '../utils/warn'
5
- import { closest , contains , isVisible , requestAF , selectAll } from '../utils/dom'
4
+ import { BvEvent } from '../utils/bv-event.class'
5
+ import { closest , contains , isVisible , requestAF , selectAll , eventOn , eventOff } from '../utils/dom'
6
6
import { isNull } from '../utils/inspect'
7
- import clickOutMixin from './click-out'
8
- import focusInMixin from './focus-in'
7
+ import idMixin from './id'
9
8
10
9
// Return an array of visible items
11
10
const filterVisibles = els => ( els || [ ] ) . filter ( isVisible )
12
11
12
+ // Root dropdown event names
13
+ const ROOT_DROPDOWN_PREFIX = 'bv::dropdown::'
14
+ const ROOT_DROPDOWN_SHOWN = `${ ROOT_DROPDOWN_PREFIX } shown`
15
+ const ROOT_DROPDOWN_HIDDEN = `${ ROOT_DROPDOWN_PREFIX } hidden`
16
+
17
+ // Delay when loosing focus before closing menu (in ms)
18
+ const FOCUSOUT_DELAY = 100
19
+
13
20
// Dropdown item CSS selectors
14
21
const Selector = {
15
22
FORM_CHILD : '.dropdown form' ,
@@ -40,7 +47,7 @@ const AttachmentMap = {
40
47
41
48
// @vue /component
42
49
export default {
43
- mixins : [ clickOutMixin , focusInMixin ] ,
50
+ mixins : [ idMixin ] ,
44
51
provide ( ) {
45
52
return {
46
53
bvDropdown : this
@@ -136,7 +143,8 @@ export default {
136
143
cancelable : true ,
137
144
vueTarget : this ,
138
145
target : this . $refs . menu ,
139
- relatedTarget : null
146
+ relatedTarget : null ,
147
+ componentId : this . safeId ? this . safeId ( ) : this . id || null
140
148
} )
141
149
this . emitEvent ( bvEvt )
142
150
if ( bvEvt . defaultPrevented ) {
@@ -181,16 +189,13 @@ export default {
181
189
emitEvent ( bvEvt ) {
182
190
const type = bvEvt . type
183
191
this . $emit ( type , bvEvt )
184
- this . $root . $emit ( `bv::dropdown:: ${ type } ` , bvEvt )
192
+ this . $root . $emit ( `${ ROOT_DROPDOWN_PREFIX } ${ type } ` , bvEvt )
185
193
} ,
186
194
showMenu ( ) {
187
195
if ( this . disabled ) {
188
196
/* istanbul ignore next */
189
197
return
190
198
}
191
- // Ensure other menus are closed
192
- this . $root . $emit ( 'bv::dropdown::shown' , this )
193
-
194
199
// Are we in a navbar ?
195
200
if ( isNull ( this . inNavbar ) && this . isNav ) {
196
201
// We should use an injection for this
@@ -213,6 +218,9 @@ export default {
213
218
}
214
219
}
215
220
221
+ // Ensure other menus are closed
222
+ this . $root . $emit ( ROOT_DROPDOWN_SHOWN , this )
223
+
216
224
this . whileOpenListen ( true )
217
225
218
226
// Wrap in nextTick to ensure menu is fully rendered/shown
@@ -225,7 +233,7 @@ export default {
225
233
} ,
226
234
hideMenu ( ) {
227
235
this . whileOpenListen ( false )
228
- this . $root . $emit ( 'bv::dropdown::hidden' , this )
236
+ this . $root . $emit ( ROOT_DROPDOWN_HIDDEN , this )
229
237
this . $emit ( 'hidden' )
230
238
this . removePopper ( )
231
239
} ,
@@ -263,19 +271,16 @@ export default {
263
271
}
264
272
return { ...popperConfig , ...( this . popperOpts || { } ) }
265
273
} ,
266
- whileOpenListen ( open ) {
274
+ whileOpenListen ( isOpen ) {
267
275
// turn listeners on/off while open
268
- if ( open ) {
276
+ if ( isOpen ) {
269
277
// If another dropdown is opened
270
- this . $root . $on ( 'bv::dropdown::shown' , this . rootCloseListener )
271
- // Hide the dropdown when clicked outside
272
- this . listenForClickOut = true
273
- // Hide the dropdown when it loses focus
274
- this . listenForFocusIn = true
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 } )
275
281
} else {
276
- this . $root . $off ( 'bv::dropdown::shown' , this . rootCloseListener )
277
- this . listenForClickOut = false
278
- this . listenForFocusIn = false
282
+ this . $root . $off ( ROOT_DROPDOWN_SHOWN , this . rootCloseListener )
283
+ eventOff ( this . $el , 'focusout' , this . onFocusOut , { passive : true } )
279
284
}
280
285
} ,
281
286
rootCloseListener ( vm ) {
@@ -360,6 +365,7 @@ export default {
360
365
this . focusNext ( evt , true )
361
366
}
362
367
} ,
368
+ // If uses presses ESC to close menu
363
369
onEsc ( evt ) {
364
370
if ( this . visible ) {
365
371
this . visible = false
@@ -369,18 +375,25 @@ export default {
369
375
this . $once ( 'hidden' , this . focusToggler )
370
376
}
371
377
} ,
372
- // Document click out listener
373
- clickOutHandler ( ) {
374
- if ( this . visible ) {
375
- this . visible = false
376
- }
377
- } ,
378
- // Document focusin listener
379
- focusInHandler ( evt ) {
380
- const target = evt . target
381
- // If focus leaves dropdown, hide it
382
- if ( this . visible && ! contains ( this . $refs . menu , target ) && ! contains ( this . toggler , target ) ) {
383
- this . visible = false
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
+ ) {
388
+ const doHide = ( ) => {
389
+ this . visible = false
390
+ }
391
+ // When we are in a navbar (which has been responsively stacked), we
392
+ // delay the dropdown's closing so that the next element has a chance
393
+ // to have it's click handler fired (in case it's position moves on
394
+ // the screen do to a navbar menu above it collapsing)
395
+ // https://github.com/bootstrap-vue/bootstrap-vue/issues/4113
396
+ this . inNavbar ? setTimeout ( doHide , FOCUSOUT_DELAY ) : doHide ( )
384
397
}
385
398
} ,
386
399
// Keyboard nav
0 commit comments