Skip to content

Commit 092ab2d

Browse files
authored
fix(modal): exclude document.body when determining return focus element (bootstrap-vue#3228)
1 parent 1bfdde6 commit 092ab2d

File tree

2 files changed

+25
-76
lines changed

2 files changed

+25
-76
lines changed

src/components/modal/modal.js

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -386,16 +386,8 @@ export default Vue.extend({
386386
return
387387
}
388388
this.is_opening = true
389-
// Note: On IE11, `document.activeElement` may be null. So we test it for
390-
// truthyness first.
391-
// https://github.com/bootstrap-vue/bootstrap-vue/issues/3206
392-
if (isBrowser && document.activeElement && document.activeElement.focus) {
393-
// Preset the fallback return focus value if it is not set
394-
// `document.activeElement` should be the trigger element that was clicked or
395-
// in the case of using the v-model, which ever element has current focus
396-
// Will be overridden by some commands such as toggle, etc.
397-
this.return_focus = this.return_focus || document.activeElement
398-
}
389+
// Set the element to return focus to when closed
390+
this.return_focus = this.return_focus || this.getActiveElement()
399391
const showEvt = new BvModalEvent('show', {
400392
cancelable: true,
401393
vueTarget: this,
@@ -465,6 +457,25 @@ export default Vue.extend({
465457
this.show()
466458
}
467459
},
460+
// Private method to get the current document active element
461+
getActiveElement() {
462+
if (isBrowser) {
463+
const activeElement = document.activeElement
464+
// Note: On IE11, `document.activeElement` may be null. So we test it for
465+
// truthyness first.
466+
// https://github.com/bootstrap-vue/bootstrap-vue/issues/3206
467+
// Returning focus to document.body may cause unwanted scrolls, so we
468+
// exclude setting focus on body
469+
if (activeElement && activeElement !== document.body && activeElement.focus) {
470+
// Preset the fallback return focus value if it is not set
471+
// `document.activeElement` should be the trigger element that was clicked or
472+
// in the case of using the v-model, which ever element has current focus
473+
// Will be overridden by some commands such as toggle, etc.
474+
return activeElement
475+
}
476+
}
477+
return null
478+
},
468479
// Private method to finish showing modal
469480
doShow() {
470481
/* istanbul ignore next: commenting out for now until we can test stacking */
@@ -631,7 +642,7 @@ export default Vue.extend({
631642
// Root listener handlers
632643
showHandler(id, triggerEl) {
633644
if (id === this.id) {
634-
this.return_focus = triggerEl || document.activeElement || null
645+
this.return_focus = triggerEl || this.getActiveElement()
635646
this.show()
636647
}
637648
},
@@ -658,7 +669,7 @@ export default Vue.extend({
658669
// Don't try and focus if we are SSR
659670
if (isBrowser) {
660671
const modal = this.$refs.modal
661-
const activeElement = document.activeElement
672+
const activeElement = this.getActiveElement()
662673
// If the modal contains the activeElement, we don't do anything
663674
if (modal && !(activeElement && contains(modal, activeElement))) {
664675
// Make sure top of modal is showing (if longer than the viewport)
@@ -673,7 +684,7 @@ export default Vue.extend({
673684
returnFocusTo() {
674685
// Prefer `returnFocus` prop over event specified
675686
// `return_focus` value
676-
let el = this.returnFocus || this.return_focus || document.activeElement || null
687+
let el = this.returnFocus || this.return_focus || null
677688
// Is el a string CSS selector?
678689
el = isString(el) ? select(el) : el
679690
if (el) {

src/components/modal/modal.spec.js

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -968,68 +968,6 @@ describe('modal', () => {
968968
describe('focus management', () => {
969969
const localVue = new CreateLocalVue()
970970

971-
it('returns focus to document.body when no return focus set and not using v-b-toggle', async () => {
972-
// JSDOM won't focus the document unless it has a tab index
973-
document.body.tabIndex = 0
974-
975-
const wrapper = mount(BModal, {
976-
attachToDocument: true,
977-
localVue: localVue,
978-
stubs: {
979-
transition: false
980-
},
981-
propsData: {
982-
static: true,
983-
id: 'test',
984-
visible: false
985-
}
986-
})
987-
988-
expect(wrapper.isVueInstance()).toBe(true)
989-
990-
await waitNT(wrapper.vm)
991-
await waitRAF()
992-
await waitNT(wrapper.vm)
993-
await waitRAF()
994-
995-
const $modal = wrapper.find('div.modal')
996-
expect($modal.exists()).toBe(true)
997-
998-
expect($modal.element.style.display).toEqual('none')
999-
expect(document.activeElement).toBe(document.body)
1000-
1001-
// Try and open modal via `.toggle()` method
1002-
wrapper.vm.toggle()
1003-
1004-
await waitNT(wrapper.vm)
1005-
await waitRAF()
1006-
await waitNT(wrapper.vm)
1007-
await waitRAF()
1008-
await waitNT(wrapper.vm)
1009-
await waitNT(wrapper.vm)
1010-
1011-
// Modal should now be open
1012-
expect($modal.element.style.display).toEqual('')
1013-
expect(document.activeElement).not.toBe(document.body)
1014-
expect(wrapper.element.contains(document.activeElement)).toBe(true)
1015-
1016-
// Try and close modal via `.toggle()` method
1017-
wrapper.vm.toggle()
1018-
1019-
await waitNT(wrapper.vm)
1020-
await waitRAF()
1021-
await waitNT(wrapper.vm)
1022-
await waitRAF()
1023-
await waitNT(wrapper.vm)
1024-
await waitNT(wrapper.vm)
1025-
1026-
// Modal should now be closed
1027-
expect($modal.element.style.display).toEqual('none')
1028-
expect(document.activeElement).toBe(document.body)
1029-
1030-
wrapper.destroy()
1031-
})
1032-
1033971
it('returns focus to previous active element when return focus not set and not using v-b-toggle', async () => {
1034972
const App = localVue.extend({
1035973
render(h) {
@@ -1186,7 +1124,7 @@ describe('modal', () => {
11861124
wrapper.destroy()
11871125
})
11881126

1189-
it('if focus leave modal it returns to modal', async () => {
1127+
it('if focus leaves modal it returns to modal', async () => {
11901128
const App = localVue.extend({
11911129
render(h) {
11921130
return h('div', {}, [

0 commit comments

Comments
 (0)