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 -->
|
<!-- 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user