feat(10-11): teleport FolderRow three-dot menu to body with getBoundingClientRect positioning

- Add menuTriggerEl ref, menuStyle, and updateMenuPosition() using getBoundingClientRect
- Wrap three-dot menu in Teleport to body with data-test="folder-row-menu"
- toggleMenu() captures position before opening
- Add window scroll/resize listeners with cleanup in onUnmounted
- Update outside-click handler to also exclude teleported menu element
- All 4 UX-13 dropdown tests now pass; full test suite 208/208 green
This commit is contained in:
curo1305
2026-06-16 08:25:30 +02:00
parent e0606b49f1
commit 892abca8cf
+50 -6
View File
@@ -36,6 +36,7 @@
<!-- Three-dot menu --> <!-- Three-dot menu -->
<div class="relative shrink-0" @click.stop> <div class="relative shrink-0" @click.stop>
<button <button
ref="menuTriggerEl"
@click="toggleMenu" @click="toggleMenu"
aria-label="Folder actions" aria-label="Folder actions"
class="p-1.5 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100 transition-colors min-h-[36px] min-w-[36px] flex items-center justify-center" class="p-1.5 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100 transition-colors min-h-[36px] min-w-[36px] flex items-center justify-center"
@@ -44,11 +45,17 @@
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zm0 6a2 2 0 110-4 2 2 0 010 4zm0 6a2 2 0 110-4 2 2 0 010 4z" /> <path d="M10 6a2 2 0 110-4 2 2 0 010 4zm0 6a2 2 0 110-4 2 2 0 010 4zm0 6a2 2 0 110-4 2 2 0 010 4z" />
</svg> </svg>
</button> </button>
</div>
</div>
<!-- Dropdown menu --> <!-- Teleported three-dot menu avoids overflow:hidden clipping -->
<Teleport to="body">
<div <div
v-if="menuOpen" v-if="menuOpen"
class="absolute right-0 top-full mt-1 bg-white border border-gray-200 rounded-lg shadow-md py-1 min-w-[140px] z-10" :style="menuStyle"
data-test="folder-row-menu"
class="fixed z-[9999] bg-white border border-gray-200 rounded-lg shadow-md py-1 min-w-[140px]"
@click.stop
> >
<button <button
@click="startRename" @click="startRename"
@@ -63,8 +70,7 @@
Delete folder Delete folder
</button> </button>
</div> </div>
</div> </Teleport>
</div>
</template> </template>
<script setup> <script setup>
@@ -90,6 +96,8 @@ const props = defineProps({
}) })
const menuOpen = ref(false) const menuOpen = ref(false)
const menuTriggerEl = ref(null)
const menuStyle = ref({})
const renaming = ref(false) const renaming = ref(false)
const renameValue = ref('') const renameValue = ref('')
const renameError = ref('') const renameError = ref('')
@@ -100,8 +108,33 @@ function handleNavigate() {
if (props.onNavigate) props.onNavigate(props.folder.id) if (props.onNavigate) props.onNavigate(props.folder.id)
} }
function updateMenuPosition() {
if (!menuTriggerEl.value) return
const rect = menuTriggerEl.value.getBoundingClientRect()
const spaceBelow = window.innerHeight - rect.bottom
const dropH = 100 // approximate height of 2-item menu
if (spaceBelow >= dropH || spaceBelow > 60) {
menuStyle.value = {
top: `${rect.bottom + 4}px`,
left: `${Math.max(0, rect.right - 160)}px`,
minWidth: '140px',
}
} else {
menuStyle.value = {
bottom: `${window.innerHeight - rect.top + 4}px`,
left: `${Math.max(0, rect.right - 160)}px`,
minWidth: '140px',
}
}
}
function toggleMenu() { function toggleMenu() {
menuOpen.value = !menuOpen.value if (menuOpen.value) {
menuOpen.value = false
} else {
updateMenuPosition()
menuOpen.value = true
}
} }
function closeMenu() { function closeMenu() {
@@ -146,16 +179,27 @@ function handleDelete() {
// Close menu on outside click // Close menu on outside click
function handleOutsideClick(e) { function handleOutsideClick(e) {
if (!e.target.closest('.relative')) { if (
!e.target.closest('[data-test="folder-row-menu"]') &&
!e.target.closest('.relative')
) {
closeMenu() closeMenu()
} }
} }
function onScroll() {
if (menuOpen.value) updateMenuPosition()
}
onMounted(() => { onMounted(() => {
document.addEventListener('click', handleOutsideClick) document.addEventListener('click', handleOutsideClick)
window.addEventListener('scroll', onScroll, true)
window.addEventListener('resize', onScroll)
}) })
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('click', handleOutsideClick) document.removeEventListener('click', handleOutsideClick)
window.removeEventListener('scroll', onScroll, true)
window.removeEventListener('resize', onScroll)
}) })
</script> </script>