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:
@@ -36,6 +36,7 @@
|
||||
<!-- Three-dot menu -->
|
||||
<div class="relative shrink-0" @click.stop>
|
||||
<button
|
||||
ref="menuTriggerEl"
|
||||
@click="toggleMenu"
|
||||
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"
|
||||
@@ -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" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<!-- Teleported three-dot menu — avoids overflow:hidden clipping -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
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
|
||||
@click="startRename"
|
||||
@@ -63,8 +70,7 @@
|
||||
Delete folder
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -90,6 +96,8 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const menuOpen = ref(false)
|
||||
const menuTriggerEl = ref(null)
|
||||
const menuStyle = ref({})
|
||||
const renaming = ref(false)
|
||||
const renameValue = ref('')
|
||||
const renameError = ref('')
|
||||
@@ -100,8 +108,33 @@ function handleNavigate() {
|
||||
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() {
|
||||
menuOpen.value = !menuOpen.value
|
||||
if (menuOpen.value) {
|
||||
menuOpen.value = false
|
||||
} else {
|
||||
updateMenuPosition()
|
||||
menuOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
@@ -146,16 +179,27 @@ function handleDelete() {
|
||||
|
||||
// Close menu on outside click
|
||||
function handleOutsideClick(e) {
|
||||
if (!e.target.closest('.relative')) {
|
||||
if (
|
||||
!e.target.closest('[data-test="folder-row-menu"]') &&
|
||||
!e.target.closest('.relative')
|
||||
) {
|
||||
closeMenu()
|
||||
}
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
if (menuOpen.value) updateMenuPosition()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleOutsideClick)
|
||||
window.addEventListener('scroll', onScroll, true)
|
||||
window.addEventListener('resize', onScroll)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleOutsideClick)
|
||||
window.removeEventListener('scroll', onScroll, true)
|
||||
window.removeEventListener('resize', onScroll)
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user