fix: use virtual-modal-container (#1709)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- Bug Fixes
- Ensured modals consistently render by using a dedicated container,
reducing cases where dialogs failed to open or appeared in the wrong
place.
- Improved reliability of modal mounting during page load and
navigation.

- Refactor
- Simplified the modal mounting mechanism to improve stability and
reduce reliance on DOM structure assumptions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Eli Bosley
2025-09-15 16:04:03 -04:00
committed by GitHub
parent 3f5039c342
commit 44b4d77d80
2 changed files with 71 additions and 170 deletions

View File

@@ -1,169 +1,77 @@
import useTeleport from '@/composables/useTeleport';
import { mount } from '@vue/test-utils';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// Mock Vue's computed while preserving other exports
vi.mock('vue', async () => ({
...(await vi.importActual('vue')),
computed: vi.fn((fn) => {
const result = { value: fn() };
return result;
}),
}));
import { defineComponent } from 'vue';
describe('useTeleport', () => {
beforeEach(() => {
// Clear the DOM before each test
document.body.innerHTML = '';
document.head.innerHTML = '';
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
// Clean up virtual container if it exists
const virtualContainer = document.getElementById('unraid-api-modals-virtual');
if (virtualContainer) {
virtualContainer.remove();
}
// Reset the module to clear the virtualModalContainer variable
vi.resetModules();
});
it('should return teleportTarget computed property', () => {
it('should return teleportTarget ref with correct value', () => {
const { teleportTarget } = useTeleport();
expect(teleportTarget).toBeDefined();
expect(teleportTarget).toHaveProperty('value');
expect(teleportTarget.value).toBe('#unraid-api-modals-virtual');
});
it('should return #modals when element with id="modals" exists', () => {
// Create element with id="modals"
const modalsDiv = document.createElement('div');
modalsDiv.id = 'modals';
document.body.appendChild(modalsDiv);
it('should create virtual container element on mount with correct properties', () => {
const TestComponent = defineComponent({
setup() {
const { teleportTarget } = useTeleport();
return { teleportTarget };
},
template: '<div>{{ teleportTarget }}</div>',
});
const { teleportTarget } = useTeleport();
expect(teleportTarget.value).toBe('#modals');
// Initially, virtual container should not exist
expect(document.getElementById('unraid-api-modals-virtual')).toBeNull();
// Mount the component
mount(TestComponent);
// After mount, virtual container should be created with correct properties
const virtualContainer = document.getElementById('unraid-api-modals-virtual');
expect(virtualContainer).toBeTruthy();
expect(virtualContainer?.className).toBe('unapi');
expect(virtualContainer?.style.position).toBe('relative');
expect(virtualContainer?.style.zIndex).toBe('999999');
expect(virtualContainer?.parentElement).toBe(document.body);
});
it('should prioritize #modals id over mounted unraid-modals', () => {
// Create both elements
const modalsDiv = document.createElement('div');
modalsDiv.id = 'modals';
document.body.appendChild(modalsDiv);
it('should reuse existing virtual container within same test', () => {
// Manually create the container first
const manualContainer = document.createElement('div');
manualContainer.id = 'unraid-api-modals-virtual';
manualContainer.className = 'unapi';
manualContainer.style.position = 'relative';
manualContainer.style.zIndex = '999999';
document.body.appendChild(manualContainer);
const unraidModals = document.createElement('unraid-modals');
unraidModals.setAttribute('data-vue-mounted', 'true');
document.body.appendChild(unraidModals);
const TestComponent = defineComponent({
setup() {
const { teleportTarget } = useTeleport();
return { teleportTarget };
},
template: '<div>{{ teleportTarget }}</div>',
});
const { teleportTarget } = useTeleport();
expect(teleportTarget.value).toBe('#modals');
});
// Mount component - should not create a new container
mount(TestComponent);
it('should return mounted unraid-modals with inner #modals div', () => {
// Create mounted unraid-modals with inner modals div
const unraidModals = document.createElement('unraid-modals');
unraidModals.setAttribute('data-vue-mounted', 'true');
const innerModals = document.createElement('div');
innerModals.id = 'modals';
unraidModals.appendChild(innerModals);
document.body.appendChild(unraidModals);
const { teleportTarget } = useTeleport();
expect(teleportTarget.value).toBe('#modals');
});
it('should add id to mounted unraid-modals when no inner modals div exists', () => {
// Create mounted unraid-modals without inner div
const unraidModals = document.createElement('unraid-modals');
unraidModals.setAttribute('data-vue-mounted', 'true');
document.body.appendChild(unraidModals);
const { teleportTarget } = useTeleport();
expect(unraidModals.id).toBe('unraid-modals-teleport-target');
expect(teleportTarget.value).toBe('#unraid-modals-teleport-target');
});
it('should use existing id of mounted unraid-modals if present', () => {
// Create mounted unraid-modals with existing id
const unraidModals = document.createElement('unraid-modals');
unraidModals.setAttribute('data-vue-mounted', 'true');
unraidModals.id = 'custom-modals-id';
document.body.appendChild(unraidModals);
const { teleportTarget } = useTeleport();
expect(teleportTarget.value).toBe('#custom-modals-id');
});
it('should ignore unmounted unraid-modals elements', () => {
// Create unmounted unraid-modals (without data-vue-mounted attribute)
const unraidModals = document.createElement('unraid-modals');
document.body.appendChild(unraidModals);
const { teleportTarget } = useTeleport();
expect(teleportTarget.value).toBe('body');
});
it('should ignore unraid-modals with data-vue-mounted="false"', () => {
// Create unraid-modals with data-vue-mounted="false"
const unraidModals = document.createElement('unraid-modals');
unraidModals.setAttribute('data-vue-mounted', 'false');
document.body.appendChild(unraidModals);
const { teleportTarget } = useTeleport();
expect(teleportTarget.value).toBe('body');
});
it('should return body as fallback when no suitable target exists', () => {
// No elements in DOM
const { teleportTarget } = useTeleport();
expect(teleportTarget.value).toBe('body');
});
it('should handle multiple unraid-modals elements correctly', () => {
// Create multiple unraid-modals, only one mounted
const unmountedModals1 = document.createElement('unraid-modals');
document.body.appendChild(unmountedModals1);
const mountedModals = document.createElement('unraid-modals');
mountedModals.setAttribute('data-vue-mounted', 'true');
mountedModals.id = 'mounted-modals';
document.body.appendChild(mountedModals);
const unmountedModals2 = document.createElement('unraid-modals');
document.body.appendChild(unmountedModals2);
const { teleportTarget } = useTeleport();
expect(teleportTarget.value).toBe('#mounted-modals');
});
it('should handle nested modal elements correctly', () => {
// Create nested structure
const container = document.createElement('div');
const unraidModals = document.createElement('unraid-modals');
unraidModals.setAttribute('data-vue-mounted', 'true');
const innerDiv = document.createElement('div');
const innerModals = document.createElement('div');
innerModals.id = 'modals';
innerDiv.appendChild(innerModals);
unraidModals.appendChild(innerDiv);
container.appendChild(unraidModals);
document.body.appendChild(container);
const { teleportTarget } = useTeleport();
expect(teleportTarget.value).toBe('#modals');
});
it('should be reactive to DOM changes', () => {
const { teleportTarget } = useTeleport();
// Initially should be body
expect(teleportTarget.value).toBe('body');
// Add modals element
const modalsDiv = document.createElement('div');
modalsDiv.id = 'modals';
document.body.appendChild(modalsDiv);
// Recreate the composable to test updated DOM state
const { teleportTarget: newTeleportTarget } = useTeleport();
expect(newTeleportTarget.value).toBe('#modals');
// Should still have only one container
const containers = document.querySelectorAll('#unraid-api-modals-virtual');
expect(containers.length).toBe(1);
expect(containers[0]).toBe(manualContainer);
});
});

View File

@@ -1,31 +1,24 @@
import { computed } from 'vue';
import { onMounted, ref } from 'vue';
let virtualModalContainer: HTMLDivElement | null = null;
const ensureVirtualContainer = () => {
if (!virtualModalContainer) {
virtualModalContainer = document.createElement('div');
virtualModalContainer.id = 'unraid-api-modals-virtual';
virtualModalContainer.className = 'unapi';
virtualModalContainer.style.position = 'relative';
virtualModalContainer.style.zIndex = '999999';
document.body.appendChild(virtualModalContainer);
}
return virtualModalContainer;
};
const useTeleport = () => {
// Computed property that finds the correct teleport target
const teleportTarget = computed(() => {
// #modals should be unique (id), but let's be defensive
const modalsElement = document.getElementById('modals');
if (modalsElement) return `#modals`;
const teleportTarget = ref<string>('#unraid-api-modals-virtual');
// Find only mounted unraid-modals components (data-vue-mounted="true")
// This ensures we don't target unmounted or duplicate elements
const mountedModals = document.querySelector('unraid-modals[data-vue-mounted="true"]');
if (mountedModals) {
// Check if it has the inner #modals div
const innerModals = mountedModals.querySelector('#modals');
if (innerModals && innerModals.id) {
return `#${innerModals.id}`;
}
// Use the mounted component itself as fallback
// Add a unique identifier if it doesn't have one
if (!mountedModals.id) {
mountedModals.id = 'unraid-modals-teleport-target';
}
return `#${mountedModals.id}`;
}
// Final fallback to body - modals component not mounted yet
return 'body';
onMounted(() => {
ensureVirtualContainer();
});
return {