Skip to content

Commit 752661a

Browse files
committed
fix(b-tooltip): Updated tooltip to work under shadowDOM
Added to dom utility methods - isConnectedToDOM() checks if a target element is in the DOM and will check both Shadow and Regular DOM - getShadowRootOrRoot() will return the target's root either the Shadow Root or DOCUMENT.body Updated isVisibile() dom util to use new isConnectedToDOM() function Updated the dom.spec.js unit tests for the two new dom utilities Fixed the dom.spec.js to get the select() and selectAll() tests working
1 parent fab4161 commit 752661a

File tree

3 files changed

+56
-72
lines changed

3 files changed

+56
-72
lines changed

src/components/tooltip/helpers/bv-tooltip.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ import {
2929
contains,
3030
getAttr,
3131
getById,
32+
getShadowRootOrRoot,
3233
hasAttr,
3334
hasClass,
35+
isConnectedToDOM,
3436
isDisabled,
3537
isElement,
3638
isVisible,
@@ -262,7 +264,7 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
262264

263265
this.$nextTick(() => {
264266
const target = this.getTarget()
265-
if (target && contains(document.body, target)) {
267+
if (target && (target.isConnected || isConnectedToDOM(target))) {
266268
// Copy the parent's scoped style attribute
267269
this.scopeId = getScopeId(this.$parent)
268270
// Set up all trigger handlers and listeners
@@ -420,7 +422,7 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
420422
const target = this.getTarget()
421423
if (
422424
!target ||
423-
!contains(document.body, target) ||
425+
!isConnectedToDOM(target) ||
424426
!isVisible(target) ||
425427
this.dropdownOpen() ||
426428
((isUndefinedOrNull(this.title) || this.title === '') &&
@@ -567,8 +569,9 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
567569
getContainer() {
568570
// Handle case where container may be a component ref
569571
const container = this.container ? this.container.$el || this.container : false
570-
const body = document.body
571572
const target = this.getTarget()
573+
const body = getShadowRootOrRoot(target)
574+
572575
// If we are in a modal, we append to the modal, If we
573576
// are in a sidebar, we append to the sidebar, else append
574577
// to body, unless a container is specified

src/utils/dom.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export const isActiveElement = el => isElement(el) && el === getActiveElement()
8383

8484
// Determine if an HTML element is visible - Faster than CSS check
8585
export const isVisible = el => {
86-
if (!isElement(el) || !el.parentNode || !contains(DOCUMENT.body, el)) {
86+
if (!isElement(el) || !el.parentNode || !isConnectedToDOM(el)) {
8787
// Note this can fail for shadow dom elements since they
8888
// are not a direct descendant of document.body
8989
return false
@@ -100,6 +100,23 @@ export const isVisible = el => {
100100
return !!(bcr && bcr.height > 0 && bcr.width > 0)
101101
}
102102

103+
// used to grab either the shadow root in a web component or the main document body
104+
export const getShadowRootOrRoot = el => {
105+
if (el.getRootNode == null) {
106+
return DOCUMENT.body
107+
}
108+
const root = el.getRootNode()
109+
if (root.nodeType === 9) {
110+
return root.body
111+
}
112+
return root
113+
}
114+
115+
export const isConnectedToDOM = el => {
116+
// If node.isConnected undefined then fallback to IE11 compliant check
117+
return el.isConnected == null ? contains(DOCUMENT.body, el) : el.isConnected
118+
}
119+
103120
// Determine if an element is disabled
104121
export const isDisabled = el =>
105122
!isElement(el) || el.disabled || hasAttr(el, 'disabled') || hasClass(el, 'disabled')

src/utils/dom.spec.js

Lines changed: 32 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import {
33
closest,
44
contains,
55
getAttr,
6+
getShadowRootOrRoot,
67
getStyle,
78
hasAttr,
89
hasClass,
10+
isConnectedToDOM,
911
isDisabled,
1012
isElement,
1113
matches,
@@ -25,28 +27,30 @@ const template = `
2527
</div>
2628
</div>
2729
`
28-
const App = { template }
30+
let App
31+
let wrapper
2932

3033
describe('utils/dom', () => {
31-
it('isElement() works', async () => {
32-
const wrapper = mount(App, {
34+
beforeEach(() => {
35+
App = { template }
36+
wrapper = mount(App, {
3337
attachTo: document.body
3438
})
39+
})
40+
41+
afterEach(() => {
42+
wrapper.destroy()
43+
})
3544

45+
it('isElement() works', async () => {
3646
expect(wrapper).toBeDefined()
3747
expect(wrapper.find('div.foo').exists()).toBe(true)
3848
expect(isElement(wrapper.element)).toBe(true)
3949
expect(isElement(null)).toBe(false)
4050
expect(isElement(App)).toBe(false)
41-
42-
wrapper.destroy()
4351
})
4452

4553
it('isDisabled() works', async () => {
46-
const wrapper = mount(App, {
47-
attachTo: document.body
48-
})
49-
5054
expect(wrapper).toBeDefined()
5155

5256
const $btns = wrapper.findAll('div.baz > button')
@@ -55,15 +59,28 @@ describe('utils/dom', () => {
5559
expect(isDisabled($btns.at(0).element)).toBe(false)
5660
expect(isDisabled($btns.at(1).element)).toBe(false)
5761
expect(isDisabled($btns.at(2).element)).toBe(true)
62+
})
5863

59-
wrapper.destroy()
64+
// NOTE: Need to figure out how to test against shadowDOM
65+
it('isConnectedToDOM() Regular DOM', async () => {
66+
expect(wrapper).toBeDefined()
67+
68+
const $barspan = wrapper.findAll('span.barspan')
69+
expect($barspan).toBeDefined()
70+
expect($barspan.length).toBe(1)
71+
expect(isConnectedToDOM($barspan.at(0).element)).toBe(true)
6072
})
6173

62-
it('hasClass() works', async () => {
63-
const wrapper = mount(App, {
64-
attachTo: document.body
65-
})
74+
it('getShadowRootOrRoot() Regular DOM', async () => {
75+
expect(wrapper).toBeDefined()
76+
77+
const $baz = wrapper.find('div.baz')
78+
const $documentBody = getShadowRootOrRoot($baz.element)
79+
expect($documentBody).toBeDefined()
80+
expect($documentBody.toString()).toBe('[object HTMLBodyElement]')
81+
})
6682

83+
it('hasClass() works', async () => {
6784
expect(wrapper).toBeDefined()
6885

6986
const $span = wrapper.find('span.barspan')
@@ -73,15 +90,9 @@ describe('utils/dom', () => {
7390
expect(hasClass($span.element, 'foobar')).toBe(true)
7491
expect(hasClass($span.element, 'fizzle-rocks')).toBe(false)
7592
expect(hasClass(null, 'foobar')).toBe(false)
76-
77-
wrapper.destroy()
7893
})
7994

8095
it('contains() works', async () => {
81-
const wrapper = mount(App, {
82-
attachTo: document.body
83-
})
84-
8596
expect(wrapper).toBeDefined()
8697

8798
const $span = wrapper.find('span.barspan')
@@ -95,15 +106,9 @@ describe('utils/dom', () => {
95106
expect(contains(wrapper.element, $btn1.element)).toBe(true)
96107
expect(contains($span.element, $btn1.element)).toBe(false)
97108
expect(contains(null, $btn1.element)).toBe(false)
98-
99-
wrapper.destroy()
100109
})
101110

102111
it('closest() works', async () => {
103-
const wrapper = mount(App, {
104-
attachTo: document.body
105-
})
106-
107112
expect(wrapper).toBeDefined()
108113

109114
const $btns = wrapper.findAll('div.baz > button')
@@ -122,15 +127,9 @@ describe('utils/dom', () => {
122127
expect(closest('div.not-here', $btns.at(0).element)).toBe(null)
123128
expect(closest('div.baz', $baz.element)).toBe(null)
124129
expect(closest('div.baz', $baz.element, true)).toBe($baz.element)
125-
126-
wrapper.destroy()
127130
})
128131

129132
it('matches() works', async () => {
130-
const wrapper = mount(App, {
131-
attachTo: document.body
132-
})
133-
134133
expect(wrapper).toBeDefined()
135134

136135
const $btns = wrapper.findAll('div.baz > button')
@@ -148,15 +147,9 @@ describe('utils/dom', () => {
148147
expect(matches($btns.at(0).element, 'div.bar > button')).toBe(false)
149148
expect(matches($btns.at(0).element, 'button#button1')).toBe(true)
150149
expect(matches(null, 'div.foo')).toBe(false)
151-
152-
wrapper.destroy()
153150
})
154151

155152
it('hasAttr() works', async () => {
156-
const wrapper = mount(App, {
157-
attachTo: document.body
158-
})
159-
160153
expect(wrapper).toBeDefined()
161154

162155
const $btns = wrapper.findAll('div.baz > button')
@@ -169,15 +162,9 @@ describe('utils/dom', () => {
169162
expect(hasAttr($btns.at(2).element, 'disabled')).toBe(true)
170163
expect(hasAttr($btns.at(2).element, 'role')).toBe(false)
171164
expect(hasAttr(null, 'role')).toBe(null)
172-
173-
wrapper.destroy()
174165
})
175166

176167
it('getAttr() works', async () => {
177-
const wrapper = mount(App, {
178-
attachTo: document.body
179-
})
180-
181168
expect(wrapper).toBeDefined()
182169

183170
const $btns = wrapper.findAll('div.baz > button')
@@ -193,15 +180,9 @@ describe('utils/dom', () => {
193180
expect(getAttr(null, 'role')).toBe(null)
194181
expect(getAttr($btns.at(0).element, '')).toBe(null)
195182
expect(getAttr($btns.at(0).element, undefined)).toBe(null)
196-
197-
wrapper.destroy()
198183
})
199184

200185
it('getStyle() works', async () => {
201-
const wrapper = mount(App, {
202-
attachTo: document.body
203-
})
204-
205186
expect(wrapper).toBeDefined()
206187

207188
const $span = wrapper.find('span.barspan')
@@ -210,15 +191,9 @@ describe('utils/dom', () => {
210191
expect(getStyle($span.element, 'color')).toBe('red')
211192
expect(getStyle($span.element, 'width')).toBe(null)
212193
expect(getStyle(null, 'color')).toBe(null)
213-
214-
wrapper.destroy()
215194
})
216195

217196
it('select() works', async () => {
218-
const wrapper = mount(App, {
219-
attachTo: document.body
220-
})
221-
222197
expect(wrapper).toBeDefined()
223198

224199
const $btns = wrapper.findAll('div.baz > button')
@@ -230,22 +205,15 @@ describe('utils/dom', () => {
230205
expect(select('button#button3', wrapper.element)).toBe($btns.at(2).element)
231206
expect(select('span.not-here', wrapper.element)).toBe(null)
232207

233-
// Note: It appears that `vue-test-utils` is not detaching previous
234-
// app instances and elements once the test is complete!
208+
// Without root element specified
235209
expect(select('button')).not.toBe(null)
236210
expect(select('button')).toBe($btns.at(0).element)
237211
expect(select('button#button3')).not.toBe(null)
238212
expect(select('button#button3')).toBe($btns.at(2).element)
239213
expect(select('span.not-here')).toBe(null)
240-
241-
wrapper.destroy()
242214
})
243215

244216
it('selectAll() works', async () => {
245-
const wrapper = mount(App, {
246-
attachTo: document.body
247-
})
248-
249217
expect(wrapper).toBeDefined()
250218

251219
const $btns = wrapper.findAll('div.baz > button')
@@ -268,8 +236,6 @@ describe('utils/dom', () => {
268236
expect(selectAll('div.baz button', wrapper.element)[2]).toBe($btns.at(2).element)
269237

270238
// Without root element specified (assumes document as root)
271-
// Note: It appears that `vue-test-utils` is not detaching previous
272-
// app instances and elements once the test is complete!
273239
expect(Array.isArray(selectAll('button'))).toBe(true)
274240
expect(selectAll('button')).not.toEqual([])
275241
expect(selectAll('button').length).toBe(3)
@@ -285,7 +251,5 @@ describe('utils/dom', () => {
285251
expect(selectAll('div.baz button')[0]).toBe($btns.at(0).element)
286252
expect(selectAll('div.baz button')[1]).toBe($btns.at(1).element)
287253
expect(selectAll('div.baz button')[2]).toBe($btns.at(2).element)
288-
289-
wrapper.destroy()
290254
})
291255
})

0 commit comments

Comments
 (0)