Skip to content

Commit 6bf308c

Browse files
Tim020claude
andcommitted
Fix v-b-tooltip DOM node leak in grid/list components (#1093)
BVN's v-b-tooltip directive teleports a persistent DOM node to <body> for every bound element, causing quadratic growth in ResourceAvailability (scenes × mics) and linear growth in SceneDensityHeatmap and MicAllocations. The leaked nodes also polluted Playwright text locators (broke spec 03). Replace per-cell v-b-tooltip with a single useHoverTooltip composable that drives one shared Teleport overlay using Bootstrap's existing .tooltip CSS classes, so the appearance is identical with zero persistent nodes at rest. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c13763d commit 6bf308c

4 files changed

Lines changed: 79 additions & 5 deletions

File tree

client-v3/src/components/show/config/mics/MicAllocations.vue

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,10 @@
128128
<template v-else>
129129
<div
130130
v-if="allocationByCharacter[data.item.Character]?.[scene.id] != null"
131-
v-b-tooltip.hover.top="getTooltipText(data.item.Character, scene.id)"
132131
class="allocation-cell"
133132
:class="getConflictClassForCell(data.item.Character, scene.id)"
133+
@mouseenter="showTooltip(getTooltipText(data.item.Character, scene.id), $event)"
134+
@mouseleave="hideTooltip"
134135
>
135136
{{ allocationByCharacter[data.item.Character]?.[scene.id] }}
136137
<IMdiAlert
@@ -148,6 +149,17 @@
148149
</BRow>
149150
<MicAutoPopulateModal ref="autoPopulateModalRef" @auto-populate-result="onAutoPopulateResult" />
150151
</BContainer>
152+
<Teleport to="body">
153+
<div
154+
v-if="tooltipVisible"
155+
role="tooltip"
156+
class="tooltip b-tooltip bs-tooltip-top show"
157+
:style="tooltipStyle"
158+
>
159+
<div class="tooltip-arrow" />
160+
<div class="tooltip-inner">{{ tooltipText }}</div>
161+
</div>
162+
</Teleport>
151163
</template>
152164

153165
<script setup lang="ts">
@@ -156,11 +168,13 @@ import { diff } from 'deep-object-diff';
156168
import { useShowStore } from '@/stores/show';
157169
import { useSystemStore } from '@/stores/system';
158170
import { useStatsTable } from '@/composables/useStatsTable';
171+
import { useHoverTooltip } from '@/composables/useHoverTooltip';
159172
import MicAutoPopulateModal from './MicAutoPopulateModal.vue';
160173
import type { MicConflict } from '@/js/micConflictUtils';
161174
162175
const showStore = useShowStore();
163176
const systemStore = useSystemStore();
177+
const { tooltipText, tooltipVisible, tooltipStyle, showTooltip, hideTooltip } = useHoverTooltip();
164178
const { sortedActs, sortedScenes, numScenesPerAct, getHeaderName, getCellName } = useStatsTable();
165179
166180
const selectedMic = ref<number | null>(null);

client-v3/src/components/show/config/mics/ResourceAvailability.vue

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,10 @@
6767
<div
6868
v-for="micStatus in sceneData.micStatuses"
6969
:key="`mic-${micStatus.mic.id}`"
70-
v-b-tooltip.hover.top="getMicTooltip(micStatus)"
7170
class="mic-status-item"
7271
:class="micStatus.statusClass"
72+
@mouseenter="showTooltip(getMicTooltip(micStatus), $event)"
73+
@mouseleave="hideTooltip"
7374
>
7475
<div class="mic-name">{{ micStatus.mic.name ?? `Mic ${micStatus.mic.id}` }}</div>
7576
<div v-if="micStatus.character" class="mic-character">
@@ -81,11 +82,23 @@
8182
</div>
8283
</div>
8384
</div>
85+
<Teleport to="body">
86+
<div
87+
v-if="tooltipVisible"
88+
role="tooltip"
89+
class="tooltip b-tooltip bs-tooltip-top show"
90+
:style="tooltipStyle"
91+
>
92+
<div class="tooltip-arrow" />
93+
<div class="tooltip-inner">{{ tooltipText }}</div>
94+
</div>
95+
</Teleport>
8496
</template>
8597

8698
<script setup lang="ts">
8799
import { computed } from 'vue';
88100
import { useShowStore } from '@/stores/show';
101+
import { useHoverTooltip } from '@/composables/useHoverTooltip';
89102
import type { Scene, Character } from '@/types/api/show';
90103
import type { Microphone } from '@/types/api/microphones';
91104
@@ -107,6 +120,7 @@ interface SceneAvailability {
107120
withDefaults(defineProps<{ loading?: boolean }>(), { loading: false });
108121
109122
const showStore = useShowStore();
123+
const { tooltipText, tooltipVisible, tooltipStyle, showTooltip, hideTooltip } = useHoverTooltip();
110124
111125
const scenes = computed(() => showStore.micTimelineData.scenes);
112126
const microphones = computed(() => showStore.micTimelineData.microphones);

client-v3/src/components/show/config/mics/SceneDensityHeatmap.vue

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,18 @@
5757
class="scene-bar-wrapper"
5858
>
5959
<div
60-
v-b-tooltip.hover.top="
61-
`${sceneData.scene.name}: ${sceneData.micCount} microphone${sceneData.micCount !== 1 ? 's' : ''}`
62-
"
6360
class="scene-bar"
6461
:style="{
6562
backgroundColor: getDensityColor(sceneData.micCount),
6663
height: getBarHeight(sceneData.micCount) + 'px',
6764
}"
65+
@mouseenter="
66+
showTooltip(
67+
`${sceneData.scene.name}: ${sceneData.micCount} microphone${sceneData.micCount !== 1 ? 's' : ''}`,
68+
$event
69+
)
70+
"
71+
@mouseleave="hideTooltip"
6872
>
6973
<span class="mic-count">{{ sceneData.micCount }}</span>
7074
</div>
@@ -86,11 +90,23 @@
8690
</div>
8791
</div>
8892
</div>
93+
<Teleport to="body">
94+
<div
95+
v-if="tooltipVisible"
96+
role="tooltip"
97+
class="tooltip b-tooltip bs-tooltip-top show"
98+
:style="tooltipStyle"
99+
>
100+
<div class="tooltip-arrow" />
101+
<div class="tooltip-inner">{{ tooltipText }}</div>
102+
</div>
103+
</Teleport>
89104
</template>
90105

91106
<script setup lang="ts">
92107
import { computed } from 'vue';
93108
import { useShowStore } from '@/stores/show';
109+
import { useHoverTooltip } from '@/composables/useHoverTooltip';
94110
import type { Scene } from '@/types/api/show';
95111
96112
interface SceneDensityEntry {
@@ -107,6 +123,7 @@ interface ActDensityGroup {
107123
withDefaults(defineProps<{ loading?: boolean }>(), { loading: false });
108124
109125
const showStore = useShowStore();
126+
const { tooltipText, tooltipVisible, tooltipStyle, showTooltip, hideTooltip } = useHoverTooltip();
110127
111128
const maxBarHeight = 200;
112129
const minBarHeight = 20;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ref, computed } from 'vue';
2+
3+
export function useHoverTooltip() {
4+
const tooltipText = ref('');
5+
const tooltipVisible = ref(false);
6+
const tooltipX = ref(0);
7+
const tooltipY = ref(0);
8+
9+
function showTooltip(text: string, event: MouseEvent): void {
10+
tooltipText.value = text;
11+
tooltipX.value = event.clientX;
12+
tooltipY.value = event.clientY;
13+
tooltipVisible.value = true;
14+
}
15+
16+
function hideTooltip(): void {
17+
tooltipVisible.value = false;
18+
}
19+
20+
const tooltipStyle = computed(() => ({
21+
position: 'fixed' as const,
22+
left: `${tooltipX.value + 12}px`,
23+
top: `${tooltipY.value - 30}px`,
24+
zIndex: 9999,
25+
pointerEvents: 'none' as const,
26+
}));
27+
28+
return { tooltipText, tooltipVisible, tooltipStyle, showTooltip, hideTooltip };
29+
}

0 commit comments

Comments
 (0)