Skip to content
This repository was archived by the owner on Mar 27, 2025. It is now read-only.

Commit a3cb0e8

Browse files
authored
Merge pull request bootstrap-vue-next#1054 from xvaara/popover-dom-overflow
fix(BPopover): remove hidden element from dom
2 parents 7f94ad6 + 4326249 commit a3cb0e8

File tree

6 files changed

+92
-93
lines changed

6 files changed

+92
-93
lines changed

apps/playground/src/components/Comps/TPopover.vue

+21
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,18 @@
196196
in body. {{ textValue }}
197197
</b-popover>
198198
</b-col>
199+
<b-col>
200+
<div style="height: 50vh; width: 400px; overflow-y: scroll; padding: 5em;">
201+
<div v-for="(_, i) in Array(20)" :key="i" style="height: 100px">
202+
<b-popover v-bind="vari">
203+
jee
204+
<template #target>
205+
<b-button>hover / focus</b-button>
206+
</template>
207+
</b-popover>
208+
</div>
209+
</div>
210+
</b-col>
199211
</b-row>
200212
</b-container>
201213
</template>
@@ -212,6 +224,15 @@ const value = ref(true)
212224
const textValue = ref('test <b onmouseover="alert(\'XSS testing!\')">with html</b>')
213225
const popoverPlacemet = ref<BPopoverPlacement>('left')
214226
227+
const vari = ref({
228+
title: 'foo',
229+
container: 'body',
230+
delay: {
231+
show: 0,
232+
hide: 0,
233+
},
234+
})
235+
215236
// eslint-disable-next-line no-console
216237
const consoleLog = (...args: unknown[]) => console.log(...args)
217238
</script>

packages/bootstrap-vue-next/src/components/BCollapse.vue

+2-12
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
</template>
1616

1717
<script setup lang="ts">
18-
import {computed, nextTick, onMounted, provide, readonly, ref, toRef, watch, watchEffect} from 'vue'
18+
import {computed, nextTick, onMounted, provide, readonly, ref, toRef, watch} from 'vue'
1919
import {useBooleanish, useId} from '../composables'
2020
import {useEventListener, useVModel} from '@vueuse/core'
2121
import type {Booleanish} from '../types'
22-
import {BvTriggerableEvent, collapseInjectionKey} from '../utils'
22+
import {BvTriggerableEvent, collapseInjectionKey, getTransitionDelay} from '../utils'
2323
2424
interface BCollapseProps {
2525
// appear?: Booleanish
@@ -156,16 +156,6 @@ watch([modelValue, show], () => {
156156
hide()
157157
})
158158
159-
const getTransitionDelay = (element: HTMLElement) => {
160-
const style = window.getComputedStyle(element)
161-
// if multiple durations are defined, we take the first
162-
const transitionDelay = style.transitionDelay.split(',')[0] || ''
163-
const transitionDuration = style.transitionDuration.split(',')[0] || ''
164-
const transitionDelayMs = Number(transitionDelay.slice(0, -1)) * 1000
165-
const transitionDurationMs = Number(transitionDuration.slice(0, -1)) * 1000
166-
return transitionDelayMs + transitionDurationMs
167-
}
168-
169159
onMounted(() => {
170160
if (element.value === null) return
171161
if (!modelValueBoolean.value && toggleBoolean.value) {

packages/bootstrap-vue-next/src/components/BPopover.vue

+33-66
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
<span ref="placeholder" />
33
<slot name="target" v-bind="{show, hide, toggle, showState}" />
44
<!-- TODO: fix this clunky solution when https://github.com/vuejs/core/issues/6152 is fixed -->
5-
<teleport v-if="container" :to="container">
5+
<RenderComponentOrSkip :tag="'Teleport'" :to="container" :skip="!container">
66
<div
7+
v-if="showStateInternal"
78
:id="id"
89
v-bind="$attrs"
910
ref="element"
@@ -52,57 +53,7 @@
5253
<!-- eslint-enable vue/no-v-html -->
5354
</template>
5455
</div>
55-
</teleport>
56-
<div
57-
v-else
58-
:id="id"
59-
v-bind="$attrs"
60-
ref="element"
61-
:class="computedClasses"
62-
role="tooltip"
63-
tabindex="-1"
64-
:style="{
65-
position: strategy,
66-
top: `${y ?? 0}px`,
67-
left: `${x ?? 0}px`,
68-
width: 'max-content',
69-
}"
70-
>
71-
<div
72-
ref="arrow"
73-
:class="`${tooltipBoolean ? 'tooltip' : 'popover'}-arrow`"
74-
:style="arrowStyle"
75-
data-popper-arrow
76-
/>
77-
<template v-if="title || $slots.title">
78-
<div v-if="!isHtml" :class="tooltipBoolean ? 'tooltip-inner' : 'popover-header'">
79-
<slot name="title">
80-
{{ title }}
81-
</slot>
82-
</div>
83-
<!-- eslint-disable vue/no-v-html -->
84-
<div
85-
v-else
86-
:class="tooltipBoolean ? 'tooltip-inner' : 'popover-header'"
87-
v-html="sanitizedTitle"
88-
/>
89-
<!-- eslint-enable vue/no-v-html -->
90-
</template>
91-
<template v-if="(tooltipBoolean && !$slots.title && !title) || !tooltipBoolean">
92-
<div v-if="!isHtml" :class="tooltipBoolean ? 'tooltip-inner' : 'popover-body'">
93-
<slot>
94-
{{ content }}
95-
</slot>
96-
</div>
97-
<!-- eslint-disable vue/no-v-html -->
98-
<div
99-
v-else
100-
:class="tooltipBoolean ? 'tooltip-inner' : 'popover-body'"
101-
v-html="sanitizedContent"
102-
/>
103-
<!-- eslint-enable vue/no-v-html -->
104-
</template>
105-
</div>
56+
</RenderComponentOrSkip>
10657
</template>
10758

10859
<script lang="ts" setup>
@@ -121,10 +72,15 @@ import {
12172
type Strategy,
12273
useFloating,
12374
} from '@floating-ui/vue'
124-
import {BvTriggerableEvent, IS_BROWSER, resolveBootstrapPlacement} from '../utils'
75+
import {
76+
BvTriggerableEvent,
77+
getTransitionDelay,
78+
IS_BROWSER,
79+
resolveBootstrapPlacement,
80+
} from '../utils'
12581
import {DefaultAllowlist, sanitizeHtml} from '../utils/sanitizer'
12682
import {onClickOutside, useMouseInElement} from '@vueuse/core'
127-
83+
import RenderComponentOrSkip from './RenderComponentOrSkip.vue'
12884
import {
12985
type ComponentPublicInstance,
13086
computed,
@@ -148,7 +104,7 @@ interface DelayObject {
148104
149105
interface BPopoverProps {
150106
modelValue?: Booleanish
151-
container?: string | ComponentPublicInstance<HTMLElement> | HTMLElement | null
107+
container?: string | ComponentPublicInstance<HTMLElement> | HTMLElement | undefined
152108
target?:
153109
| (() => HTMLElement | VNode)
154110
| string
@@ -192,7 +148,7 @@ const props = withDefaults(defineProps<BPopoverProps>(), {
192148
id: undefined,
193149
content: undefined,
194150
modelValue: false,
195-
container: null,
151+
container: undefined,
196152
customClass: '',
197153
placement: 'top',
198154
strategy: 'absolute',
@@ -228,6 +184,7 @@ const emit = defineEmits<BPopoverEmits>()
228184
229185
const modelValueBoolean = useBooleanish(toRef(props, 'modelValue'))
230186
const showState = ref(modelValueBoolean.value)
187+
const showStateInternal = ref(modelValueBoolean.value)
231188
watchEffect(() => {
232189
emit('update:modelValue', showState.value)
233190
})
@@ -409,16 +366,20 @@ const show = () => {
409366
emit('show-prevented')
410367
return
411368
}
412-
setTimeout(
413-
() => {
414-
update()
415-
showState.value = true
416-
nextTick(() => {
417-
emit('shown', buildTriggerableEvent('shown'))
418-
})
419-
},
420-
typeof props.delay === 'number' ? props.delay : props.delay?.show || 0
421-
)
369+
showStateInternal.value = true
370+
nextTick(() => {
371+
update()
372+
setTimeout(
373+
() => {
374+
update()
375+
showState.value = true
376+
nextTick(() => {
377+
emit('shown', buildTriggerableEvent('shown'))
378+
})
379+
},
380+
typeof props.delay === 'number' ? props.delay : props.delay?.show || 0
381+
)
382+
})
422383
}
423384
424385
const hide = (e: Event) => {
@@ -438,6 +399,12 @@ const hide = (e: Event) => {
438399
) {
439400
showState.value = false
440401
nextTick(() => {
402+
setTimeout(
403+
() => {
404+
showStateInternal.value = false
405+
},
406+
element.value ? getTransitionDelay(element.value) : 150
407+
)
441408
emit('hidden', buildTriggerableEvent('hidden'))
442409
})
443410
} else {
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import {defineComponent, h} from 'vue'
2+
import {defineComponent, h, type PropType, type RendererElement, Teleport} from 'vue'
33
44
export default defineComponent({
55
name: 'ComponentOrEmpty',
@@ -8,13 +8,22 @@ export default defineComponent({
88
type: String,
99
default: 'div',
1010
},
11+
to: {
12+
type: [String, Object] as PropType<string | RendererElement | null | undefined>,
13+
default: null,
14+
},
1115
skip: {
1216
type: Boolean,
1317
default: false,
1418
},
1519
},
1620
setup(props, {slots, attrs}) {
17-
return () => (props.skip ? slots.default?.() : h(props.tag, {...attrs}, [slots.default?.()]))
21+
return () =>
22+
props.skip
23+
? slots.default?.()
24+
: props.tag === 'Teleport'
25+
? h(Teleport, {to: props.to}, [slots.default?.()])
26+
: h(props.tag, {...attrs}, [slots.default?.()])
1827
},
1928
})
2029
</script>

packages/bootstrap-vue-next/src/components/popover.spec.ts

+15-13
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,35 @@ describe('popover', () => {
66
enableAutoUnmount(afterEach)
77

88
it('has div.popover', () => {
9-
const wrapper = mount(BPopover)
9+
const wrapper = mount(BPopover, {props: {modelValue: true}})
1010
const $div = wrapper.find('div.popover')
1111
expect($div.exists()).toBe(true)
1212
})
1313

1414
it('has static class popover', () => {
15-
const wrapper = mount(BPopover)
15+
const wrapper = mount(BPopover, {props: {modelValue: true}})
1616
const $div = wrapper.get('div.popover')
1717

1818
expect($div.classes()).toContain('popover')
1919
})
2020

2121
it('has static class b-popover', () => {
22-
const wrapper = mount(BPopover)
22+
const wrapper = mount(BPopover, {
23+
props: {modelValue: true},
24+
})
2325
const $div = wrapper.get('div.popover')
2426

2527
expect($div.classes()).toContain('b-popover')
2628
})
2729

2830
it('has role tooltip', () => {
29-
const wrapper = mount(BPopover)
31+
const wrapper = mount(BPopover, {props: {modelValue: true}})
3032
const $div = wrapper.get('div.popover')
3133
expect($div.attributes('role')).toBe('tooltip')
3234
})
3335

3436
it('has tabindex -1', () => {
35-
const wrapper = mount(BPopover)
37+
const wrapper = mount(BPopover, {props: {modelValue: true}})
3638
const $div = wrapper.get('div.popover')
3739

3840
expect($div.attributes('tabindex')).toBe('-1')
@@ -44,9 +46,7 @@ describe('popover', () => {
4446
})
4547

4648
it('has prop id', async () => {
47-
const wrapper = mount(BPopover, {
48-
props: {id: 'abc'},
49-
})
49+
const wrapper = mount(BPopover, {props: {id: 'abc', modelValue: true}})
5050
const $div = wrapper.get('div.popover')
5151

5252
expect($div.attributes('id')).toBe('abc')
@@ -56,6 +56,7 @@ describe('popover', () => {
5656

5757
it('first child contains slot title', () => {
5858
const wrapper = mount(BPopover, {
59+
props: {modelValue: true},
5960
slots: {title: 'foobar'},
6061
})
6162
const $div = wrapper.get('div.popover-header')
@@ -64,15 +65,15 @@ describe('popover', () => {
6465

6566
it('first child contains prop title', () => {
6667
const wrapper = mount(BPopover, {
67-
props: {title: 'foobar'},
68+
props: {title: 'foobar', modelValue: true},
6869
})
6970
const $div = wrapper.get('div.popover-header')
7071
expect($div.text()).toBe('foobar')
7172
})
7273

7374
it('first child contains slot title if both slot and prop exists', () => {
7475
const wrapper = mount(BPopover, {
75-
props: {title: 'propbar'},
76+
props: {title: 'propbar', modelValue: true},
7677
slots: {title: 'slotbar'},
7778
})
7879
const $div = wrapper.get('div.popover-header')
@@ -81,22 +82,23 @@ describe('popover', () => {
8182

8283
it('contains slot default', () => {
8384
const wrapper = mount(BPopover, {
85+
props: {modelValue: true},
8486
slots: {default: 'foobar'},
8587
})
8688
expect(wrapper.text()).toContain('foobar')
8789
})
8890

8991
it('second child contains prop content', () => {
9092
const wrapper = mount(BPopover, {
91-
props: {content: 'foobar'},
93+
props: {content: 'foobar', modelValue: true},
9294
})
9395
const $div = wrapper.get('div.popover-body')
9496
expect($div.text()).toBe('foobar')
9597
})
9698

9799
it('contains slot default if both slot and prop exists', () => {
98100
const wrapper = mount(BPopover, {
99-
props: {content: 'propbar'},
101+
props: {content: 'propbar', modelValue: true},
100102
slots: {default: '<div class="trigger">slotbar</div>'},
101103
})
102104
const $div = wrapper.get('div.trigger')
@@ -105,7 +107,7 @@ describe('popover', () => {
105107

106108
it('contains b-popover-{type} if prop variant', async () => {
107109
const wrapper = mount(BPopover, {
108-
props: {variant: 'primary'},
110+
props: {variant: 'primary', modelValue: true},
109111
})
110112
const $div = wrapper.get('div.popover')
111113
// console.log($div.classes())

packages/bootstrap-vue-next/src/utils/dom.ts

+10
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,13 @@ export const closest = (selector: string, root: Element, includeRoot = false) =>
172172
// the passed in root element when `includeRoot` is falsey
173173
return includeRoot ? el : el === root ? null : el
174174
}
175+
176+
export const getTransitionDelay = (element: HTMLElement) => {
177+
const style = window.getComputedStyle(element)
178+
// if multiple durations are defined, we take the first
179+
const transitionDelay = style.transitionDelay.split(',')[0] || ''
180+
const transitionDuration = style.transitionDuration.split(',')[0] || ''
181+
const transitionDelayMs = Number(transitionDelay.slice(0, -1)) * 1000
182+
const transitionDurationMs = Number(transitionDuration.slice(0, -1)) * 1000
183+
return transitionDelayMs + transitionDurationMs
184+
}

0 commit comments

Comments
 (0)