Skip to content
Permalink
Browse files
fix(b-dropdown): focus-in handling for Safari and Firefox on macOS/iOS (
closes #4328) (#4426)

* fix(b-dropdown): focus-in handling for Safari and Firefox on macOS/iOS

* Update dropdown.js

* Fix dropdown toggle focus-in handling

* Handle 'touchstart'

* Revert "Handle 'touchstart'"

This reverts commit b46ab2b.

* Remove outdated stuff

* Update dropdown.js

* Add temporary logs

* Update click-out.js

* Update dropdown.js

* Improve `inNavbar` detection by using provide/inject

* Correct typos

* add comment with link to issue

* Update dropdown.js

Co-authored-by: Troy Morehouse <troymore@nbnet.nb.ca>
  • Loading branch information
jacobmllr95 and tmorehouse committed Jan 30, 2020
1 parent 3a50ad8 commit 2eab55b4672a35a487b30f0f64c63b887b361473
@@ -708,13 +708,6 @@ The `.dropdown-menu` is the `<ul>` element, while dropdown items (items, buttons
headers, and dividers) are wrapped in an `<li>` element. If creating custom items to place inside
the dropdown menu, ensure they are wrapped with a plain `<li>`.

On touch-enabled devices, opening a `<b-dropdown>` adds empty (noop) `mouseover` handlers to the
immediate children of the `<body>` element. This admittedly ugly hack is necessary to work around a
[quirk in iOS' event delegation](https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html),
which would otherwise prevent a tap anywhere outside of the dropdown from triggering the code that
closes the dropdown. Once the dropdown is closed, these additional empty `mouseover` handlers are
removed.

## See also

- [`<b-nav-item-dropdown>`](/docs/components/nav#dropdown-support) for dropdown support inside
@@ -1,5 +1,5 @@
import Vue from '../../utils/vue'
import nomalizeSlotMixin from '../../mixins/normalize-slot'
import normalizeSlotMixin from '../../mixins/normalize-slot'

export const props = {
active: {
@@ -23,7 +23,7 @@ export const props = {
// @vue/component
export const BDropdownItemButton = /*#__PURE__*/ Vue.extend({
name: 'BDropdownItemButton',
mixins: [nomalizeSlotMixin],
mixins: [normalizeSlotMixin],
inheritAttrs: false,
inject: {
bvDropdown: {
@@ -1,14 +1,14 @@
import Vue from '../../utils/vue'
import { requestAF } from '../../utils/dom'
import nomalizeSlotMixin from '../../mixins/normalize-slot'
import normalizeSlotMixin from '../../mixins/normalize-slot'
import { BLink, propsFactory as linkPropsFactory } from '../link/link'

export const props = linkPropsFactory()

// @vue/component
export const BDropdownItem = /*#__PURE__*/ Vue.extend({
name: 'BDropdownItem',
mixins: [nomalizeSlotMixin],
mixins: [normalizeSlotMixin],
inheritAttrs: false,
inject: {
bvDropdown: {
@@ -146,7 +146,7 @@ export const BDropdown = /*#__PURE__*/ Vue.extend({
id: this.safeId('_BV_button_')
},
on: {
click: this.click
click: this.onSplitClick
}
},
[buttonContent]
@@ -171,8 +171,9 @@ export const BDropdown = /*#__PURE__*/ Vue.extend({
'aria-expanded': this.visible ? 'true' : 'false'
},
on: {
click: this.toggle, // click
keydown: this.toggle // enter, space, down
mousedown: this.onMousedown,
click: this.toggle,
keydown: this.toggle // Handle ENTER, SPACE and DOWN
}
},
[this.split ? h('span', { class: ['sr-only'] }, [this.toggleText]) : buttonContent]
@@ -189,7 +190,7 @@ export const BDropdown = /*#__PURE__*/ Vue.extend({
'aria-labelledby': this.safeId(this.split ? '_BV_button_' : '_BV_toggle_')
},
on: {
keydown: this.onKeydown // up, down, esc
keydown: this.onKeydown // Handle UP, DOWN and ESC
}
},
!this.lazy || this.visible ? this.normalizeSlot('default', { hide: this.hide }) : [h()]
@@ -54,8 +54,9 @@ export const BNavItemDropdown = /*#__PURE__*/ Vue.extend({
'aria-expanded': this.visible ? 'true' : 'false'
},
on: {
mousedown: this.onMousedown,
click: this.toggle,
keydown: this.toggle // space, enter, down
keydown: this.toggle // Handle ENTER, SPACE and DOWN
}
},
[
@@ -75,7 +76,7 @@ export const BNavItemDropdown = /*#__PURE__*/ Vue.extend({
'aria-labelledby': this.safeId('_BV_button_')
},
on: {
keydown: this.onKeydown // up, down, esc
keydown: this.onKeydown // Handle UP, DOWN and ESC
}
},
!this.lazy || this.visible ? this.normalizeSlot('default', { hide: this.hide }) : [h()]
@@ -1,7 +1,7 @@
import Vue from '../../utils/vue'
import { mergeData } from 'vue-functional-data-merge'
import { getComponentConfig, getBreakpoints } from '../../utils/config'
import { isString } from '../../utils/inspect'
import normalizeSlotMixin from '../../mixins/normalize-slot'

const NAME = 'BNavbar'

@@ -38,33 +38,45 @@ export const props = {
// @vue/component
export const BNavbar = /*#__PURE__*/ Vue.extend({
name: NAME,
functional: true,
mixins: [normalizeSlotMixin],
props,
render(h, { props, data, children }) {
let breakpoint = ''
const xs = getBreakpoints()[0]
if (props.toggleable && isString(props.toggleable) && props.toggleable !== xs) {
breakpoint = `navbar-expand-${props.toggleable}`
} else if (props.toggleable === false) {
breakpoint = 'navbar-expand'
provide() {
return { bvNavbar: this }
},
computed: {
breakpointClass() {
let breakpoint = null
const xs = getBreakpoints()[0]
const toggleable = this.toggleable
if (toggleable && isString(toggleable) && toggleable !== xs) {
breakpoint = `navbar-expand-${toggleable}`
} else if (toggleable === false) {
breakpoint = 'navbar-expand'
}

return breakpoint
}
},
render(h) {
return h(
props.tag,
mergeData(data, {
this.tag,
{
staticClass: 'navbar',
class: {
'd-print': props.print,
'sticky-top': props.sticky,
[`navbar-${props.type}`]: props.type,
[`bg-${props.variant}`]: props.variant,
[`fixed-${props.fixed}`]: props.fixed,
[`${breakpoint}`]: breakpoint
},
class: [
{
'd-print': this.print,
'sticky-top': this.sticky,
[`navbar-${this.type}`]: this.type,
[`bg-${this.variant}`]: this.variant,
[`fixed-${this.fixed}`]: this.fixed
},
this.breakpointClass
],
attrs: {
role: props.tag === 'nav' ? null : 'navigation'
role: this.tag === 'nav' ? null : 'navigation'
}
}),
children
},
[this.normalizeSlot('default')]
)
}
})
@@ -19,9 +19,7 @@ describe('navbar', () => {

it('accepts custom tag', async () => {
const wrapper = mount(BNavbar, {
context: {
props: { tag: 'div' }
}
propsData: { tag: 'div' }
})
expect(wrapper.is('div')).toBe(true)
expect(wrapper.attributes('role')).toBeDefined()
@@ -30,9 +28,7 @@ describe('navbar', () => {

it('accepts breakpoint via toggleable prop', async () => {
const wrapper = mount(BNavbar, {
context: {
props: { toggleable: 'lg' }
}
propsData: { toggleable: 'lg' }
})
expect(wrapper.classes()).toContain('navbar')
expect(wrapper.classes()).toContain('navbar-expand-lg')
@@ -42,9 +38,7 @@ describe('navbar', () => {

it('toggleable=true has expected classes', async () => {
const wrapper = mount(BNavbar, {
context: {
props: { toggleable: true }
}
propsData: { toggleable: true }
})
expect(wrapper.classes()).toContain('navbar')
expect(wrapper.classes()).toContain('navbar-light')
@@ -53,9 +47,7 @@ describe('navbar', () => {

it('toggleable=xs has expected classes', async () => {
const wrapper = mount(BNavbar, {
context: {
props: { toggleable: 'xs' }
}
propsData: { toggleable: 'xs' }
})
expect(wrapper.classes()).toContain('navbar')
expect(wrapper.classes()).toContain('navbar-light')
@@ -64,9 +56,7 @@ describe('navbar', () => {

it('has class "fixed-top" when fixed="top"', async () => {
const wrapper = mount(BNavbar, {
context: {
props: { fixed: 'top' }
}
propsData: { fixed: 'top' }
})
expect(wrapper.classes()).toContain('fixed-top')
expect(wrapper.classes()).toContain('navbar')
@@ -77,9 +67,7 @@ describe('navbar', () => {

it('has class "fixed-top" when fixed="top"', async () => {
const wrapper = mount(BNavbar, {
context: {
props: { fixed: 'top' }
}
propsData: { fixed: 'top' }
})
expect(wrapper.classes()).toContain('fixed-top')
expect(wrapper.classes()).toContain('navbar')
@@ -90,9 +78,7 @@ describe('navbar', () => {

it('has class "sticky-top" when sticky=true', async () => {
const wrapper = mount(BNavbar, {
context: {
props: { sticky: true }
}
propsData: { sticky: true }
})
expect(wrapper.classes()).toContain('sticky-top')
expect(wrapper.classes()).toContain('navbar')
@@ -103,9 +89,7 @@ describe('navbar', () => {

it('accepts variant prop', async () => {
const wrapper = mount(BNavbar, {
context: {
props: { variant: 'primary' }
}
propsData: { variant: 'primary' }
})
expect(wrapper.classes()).toContain('bg-primary')
expect(wrapper.classes()).toContain('navbar')
@@ -29,7 +29,7 @@ export default {
this.clickOutElement = document
}
if (!this.clickOutEventName) {
this.clickOutEventName = 'ontouchstart' in document.documentElement ? 'touchstart' : 'click'
this.clickOutEventName = 'click'
}
if (this.listenForClickOut) {
eventOn(this.clickOutElement, this.clickOutEventName, this._clickOutHandler, eventOptions)

0 comments on commit 2eab55b

Please sign in to comment.