Skip to content

fix(dropdown): Menu focusout close handling #2252

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/components/dropdown/dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ export default {
},
on: {
mouseover: this.onMouseOver,
focusout: this.onFocusOut, // focus out of menu
keydown: this.onKeydown // tab, up, down, esc
}
},
Expand Down
3 changes: 1 addition & 2 deletions src/components/link/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,13 @@ function clickHandlerFactory ({ disabled, tag, href, suppliedHandler, parent })
// Kill the event loop attached to this specific EventTarget.
e.stopImmediatePropagation()
} else {
parent.$root.$emit('clicked::link', e)

if (isRouterLink && e.target.__vue__) {
e.target.__vue__.$emit('click', e)
}
if (typeof suppliedHandler === 'function') {
suppliedHandler(...arguments)
}
parent.$root.$emit('clicked::link', e)
}

if ((!isRouterLink && href === '#') || disabled) {
Expand Down
1 change: 0 additions & 1 deletion src/components/nav/nav-item-dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export default {
attrs: { 'aria-labelledby': this.safeId('_BV_button_') },
on: {
mouseover: this.onMouseOver,
focusout: this.onFocusOut, // focus out of menu
keydown: this.onKeydown // tab, up, down, esc
}
},
Expand Down
48 changes: 48 additions & 0 deletions src/mixins/click-out.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { contains, eventOff, eventOn } from '../utils/dom'

export default {
data () {
return {
listenForClickOut: false
}
},
watch: {
listenForClickOut (newValue, oldValue) {
if (newValue !== oldValue) {
eventOff(this.clickOutElement, this.clickOutEventName, this._clickOutHandler, false)
if (newValue) {
eventOn(this.clickOutElement, this.clickOutEventName, this._clickOutHandler, false)
}
}
}
},
beforeCreate () {
// Declare non-reactive properties
this.clickOutElement = null
this.clickOutEventName = null
},
mounted () {
if (!this.clickOutElement) {
this.clickOutElement = document
}
if (!this.clickOutEventName) {
this.clickOutEventName = 'ontouchstart' in document.documentElement ? 'touchstart' : 'click'
}
if (this.listenForClickOut) {
eventOn(this.clickOutElement, this.clickOutEventName, this._clickOutHandler, false)
}
},
beforeDestroy () {
eventOff(this.clickOutElement, this.clickOutEventName, this._clickOutHandler, false)
},
methods: {
isClickOut (evt) {
return !contains(this.$el, evt.target)
},
_clickOutHandler (evt) {
if (this.clickOutHandler && this.isClickOut(evt)) {
this.clickOutHandler(evt)
}
}
}
}
101 changes: 53 additions & 48 deletions src/mixins/dropdown.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import Popper from 'popper.js'
import { from as arrayFrom } from '../utils/array'
import clickOutMixin from './click-out'
import focusInMixin from './focus-in'
import { assign } from '../utils/object'
import KeyCodes from '../utils/key-codes'
import BvEvent from '../utils/bv-event.class'
import warn from '../utils/warn'
import { isVisible, closest, selectAll, getAttr, eventOn, eventOff } from '../utils/dom'
import { closest, contains, getAttr, isVisible, selectAll } from '../utils/dom'

// Return an Array of visible items
function filterVisible (els) {
Expand All @@ -24,17 +25,26 @@ const Selector = {

// Popper attachment positions
const AttachmentMap = {
// DropUp Left Align
// Dropup left align
TOP: 'top-start',
// DropUp Right Align
// Dropup right align
TOPEND: 'top-end',
// Dropdown left Align
// Dropdown left align
BOTTOM: 'bottom-start',
// Dropdown Right Align
BOTTOMEND: 'bottom-end'
// Dropdown right align
BOTTOMEND: 'bottom-end',
// Dropright left align
RIGHT: 'right-start',
// Dropright right align
RIGHTEND: 'right-end',
// Dropleft left align
LEFT: 'left-start',
// Dropleft right align
LEFTEND: 'left-end'
}

export default {
mixins: [clickOutMixin, focusInMixin],
props: {
disabled: {
type: Boolean,
Expand Down Expand Up @@ -140,7 +150,8 @@ export default {
},
computed: {
toggler () {
return this.$refs.toggle.$el || this.$refs.toggle
const toggle = this.$refs.toggle
return toggle ? toggle.$el || toggle : null
}
},
methods: {
Expand Down Expand Up @@ -211,21 +222,15 @@ export default {
} else if (this.right) {
placement = AttachmentMap.BOTTOMEND
}
const popperConfig = {
let popperConfig = {
placement,
modifiers: {
offset: {
offset: this.offset || 0
},
flip: {
enabled: !this.noFlip
}
offset: { offset: this.offset || 0 },
flip: { enabled: !this.noFlip }
}
}
if (this.boundary) {
popperConfig.modifiers.preventOverflow = {
boundariesElement: this.boundary
}
popperConfig.modifiers.preventOverflow = { boundariesElement: this.boundary }
}
return assign(popperConfig, this.popperOpts || {})
},
Expand All @@ -238,36 +243,25 @@ export default {
this.$root.$on('clicked::link', this.rootCloseListener)
// Use new namespaced events for clicked
this.$root.$on('bv::link::clicked', this.rootCloseListener)
// Hide the dropdown when clicked outside
this.listenForClickOut = true
// Hide the dropdown when it loses focus
this.listenForFocusIn = true
} else {
this.$root.$off('bv::dropdown::shown', this.rootCloseListener)
this.$root.$off('clicked::link', this.rootCloseListener)
this.$root.$off('bv::link::clicked', this.rootCloseListener)
}
// touchstart handling fix
/* istanbul ignore next: not easy to test */
if ('ontouchstart' in document.documentElement) {
// If this is a touch-enabled device we add extra
// empty mouseover listeners to the body's immediate children;
// only needed because of broken event delegation on iOS
// https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
// Only enabled if we are *not* in an .navbar-nav.
const children = arrayFrom(document.body.children)
const isNavBarNav = closest(this.$el, Selector.NAVBAR_NAV)
children.forEach(el => {
if (open && !isNavBarNav) {
eventOn(el, 'mouseover', this._noop)
} else {
eventOff(el, 'mouseover', this._noop)
}
})
this.listenForClickOut = false
this.listenForFocusIn = false
}
},
_noop () /* istanbul ignore next: no need to test */ {
// Do nothing event handler (used in touchstart event handler)
},
rootCloseListener (vm) {
if (vm !== this) {
this.visible = false
// Return focus to original trigger button
this.$nextTick(() => {
this.focusToggler()
})
}
},
show () {
Expand Down Expand Up @@ -338,25 +332,36 @@ export default {
evt.preventDefault()
evt.stopPropagation()
// Return focus to original trigger button
this.$nextTick(this.focusToggler)
this.$nextTick(() => {
this.focusToggler()
})
}
},
onTab (evt) /* istanbul ignore next: not easy to test */ {
// TODO: Need special handler for dealing with form inputs
// Tab, if in a text-like input, we should just focus next item in the dropdown
// Note: Inputs are in a special .dropdown-form container
},
onFocusOut (evt) {
if (this.$el.contains(evt.relatedTarget)) {
return
}
setTimeout(() => {
this.visible = false
})
},
onMouseOver (evt) /* istanbul ignore next: not easy to test */ {
// Removed mouseover focus handler
},
// Docmunet click out listener
clickOutHandler () {
if (this.visible) {
this.visible = false
}
},
// Document focusin listener
focusInHandler (evt) {
// If focus leaves dropdown, hide it
if (
this.visible &&
!contains(this.$refs.menu, evt.target) &&
!contains(this.$refs.toggle, evt.target)
) {
this.visible = false
}
},
focusNext (evt, up) {
if (!this.visible) {
return
Expand Down
41 changes: 41 additions & 0 deletions src/mixins/focus-in.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { eventOff, eventOn } from '../utils/dom'

export default {
data () {
return {
listenForFocusIn: false
}
},
watch: {
listenForFocusIn (newValue, oldValue) {
if (newValue !== oldValue) {
eventOff(this.focusInElement, 'focusin', this._focusInHandler, false)
if (newValue) {
eventOn(this.focusInElement, 'focusin', this._focusInHandler, false)
}
}
}
},
beforeCreate () {
// Declare non-reactive properties
this.focusInElement = null
},
mounted () {
if (!this.focusInElement) {
this.focusInElement = document
}
if (this.listenForFocusIn) {
eventOn(this.focusInElement, 'focusin', this._focusInHandler, false)
}
},
beforeDestroy () {
eventOff(this.focusInElement, 'focusin', this._focusInHandler, false)
},
methods: {
_focusInHandler (evt) {
if (this.focusInHandler) {
this.focusInHandler(evt)
}
}
}
}