Skip to content

Commit 3353778

Browse files
committed
feat: use teleport to fix popover clipping for short install terminal
1 parent 669e64e commit 3353778

1 file changed

Lines changed: 54 additions & 34 deletions

File tree

app/components/PackageManagerSelect.vue

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { onClickOutside } from '@vueuse/core'
2+
import { onClickOutside, useEventListener } from '@vueuse/core'
33
44
const selectedPM = useSelectedPackageManager()
55
@@ -8,6 +8,18 @@ const triggerRef = useTemplateRef('triggerRef')
88
const isOpen = shallowRef(false)
99
const highlightedIndex = shallowRef(-1)
1010
11+
const dropdownPosition = shallowRef<{ top: number; left: number } | null>(null)
12+
13+
function getDropdownStyle(): Record<string, string> {
14+
if (!dropdownPosition.value) return {}
15+
return {
16+
top: `${dropdownPosition.value.top}px`,
17+
left: `${dropdownPosition.value.left}px`,
18+
}
19+
}
20+
21+
useEventListener('scroll', close, true)
22+
1123
// Generate unique ID for accessibility
1224
const inputId = useId()
1325
const listboxId = `${inputId}-listbox`
@@ -20,6 +32,13 @@ function toggle() {
2032
if (isOpen.value) {
2133
close()
2234
} else {
35+
if (triggerRef.value) {
36+
const rect = triggerRef.value.getBoundingClientRect()
37+
dropdownPosition.value = {
38+
top: rect.bottom + 4,
39+
left: rect.left,
40+
}
41+
}
2342
isOpen.value = true
2443
highlightedIndex.value = packageManagers.findIndex(pm => pm.id === selectedPM.value)
2544
}
@@ -70,37 +89,37 @@ function handleKeydown(event: KeyboardEvent) {
7089
</script>
7190

7291
<template>
73-
<div class="relative">
74-
<button
75-
ref="triggerRef"
76-
type="button"
77-
class="flex items-center gap-1.5 px-2 py-2 font-mono text-xs text-fg-muted bg-bg-subtle border border-border-subtle border-solid rounded-md transition-colors duration-150 hover:(text-fg border-border-hover) active:scale-95 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 hover:text-fg"
78-
:aria-expanded="isOpen"
79-
aria-haspopup="listbox"
80-
:aria-label="$t('settings.package_manager')"
81-
:aria-controls="listboxId"
82-
@click="toggle"
83-
@keydown="handleKeydown"
84-
>
85-
<ClientOnly>
86-
<span class="inline-block h-3 w-3" :class="pm.icon" aria-hidden="true" />
87-
<span>{{ pm.label }}</span>
88-
<template #fallback>
89-
<span class="inline-block h-3 w-3 i-simple-icons:npm" aria-hidden="true" />
90-
<span>npm</span>
91-
</template>
92-
</ClientOnly>
93-
<span
94-
class="i-carbon:chevron-down w-3 h-3"
95-
:class="[
96-
{ 'rotate-180': isOpen },
97-
prefersReducedMotion ? '' : 'transition-transform duration-200',
98-
]"
99-
aria-hidden="true"
100-
/>
101-
</button>
92+
<button
93+
ref="triggerRef"
94+
type="button"
95+
class="flex items-center gap-1.5 px-2 py-2 font-mono text-xs text-fg-muted bg-bg-subtle border border-border-subtle border-solid rounded-md transition-colors duration-150 hover:(text-fg border-border-hover) active:scale-95 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 hover:text-fg"
96+
:aria-expanded="isOpen"
97+
aria-haspopup="listbox"
98+
:aria-label="$t('settings.package_manager')"
99+
:aria-controls="listboxId"
100+
@click="toggle"
101+
@keydown="handleKeydown"
102+
>
103+
<ClientOnly>
104+
<span class="inline-block h-3 w-3" :class="pm.icon" aria-hidden="true" />
105+
<span>{{ pm.label }}</span>
106+
<template #fallback>
107+
<span class="inline-block h-3 w-3 i-simple-icons:npm" aria-hidden="true" />
108+
<span>npm</span>
109+
</template>
110+
</ClientOnly>
111+
<span
112+
class="i-carbon:chevron-down w-3 h-3"
113+
:class="[
114+
{ 'rotate-180': isOpen },
115+
prefersReducedMotion ? '' : 'transition-transform duration-200',
116+
]"
117+
aria-hidden="true"
118+
/>
119+
</button>
102120

103-
<!-- Dropdown menu -->
121+
<!-- Dropdown menu (teleported to body to avoid clipping) -->
122+
<Teleport to="body">
104123
<Transition
105124
:enter-active-class="prefersReducedMotion ? '' : 'transition-opacity duration-150'"
106125
:enter-from-class="prefersReducedMotion ? '' : 'opacity-0'"
@@ -120,7 +139,8 @@ function handleKeydown(event: KeyboardEvent) {
120139
: undefined
121140
"
122141
:aria-label="$t('settings.package_manager')"
123-
class="absolute inset-ie-0 top-full mt-1 bg-bg-elevated border border-border rounded-md shadow-lg z-50 py-1"
142+
:style="getDropdownStyle()"
143+
class="fixed bg-bg-subtle border border-border rounded-md shadow-lg z-50"
124144
>
125145
<li
126146
v-for="(pm, index) in packageManagers"
@@ -131,7 +151,7 @@ function handleKeydown(event: KeyboardEvent) {
131151
class="flex items-center gap-2 px-3 py-1.5 font-mono text-xs cursor-pointer transition-colors duration-150"
132152
:class="[
133153
selectedPM === pm.id ? 'text-fg' : 'text-fg-subtle',
134-
highlightedIndex === index ? 'bg-bg-subtle' : 'hover:bg-bg-subtle',
154+
highlightedIndex === index ? 'bg-bg-elevated' : 'hover:bg-bg-elevated',
135155
]"
136156
@click="select(pm.id)"
137157
@mouseenter="highlightedIndex = index"
@@ -146,5 +166,5 @@ function handleKeydown(event: KeyboardEvent) {
146166
</li>
147167
</ul>
148168
</Transition>
149-
</div>
169+
</Teleport>
150170
</template>

0 commit comments

Comments
 (0)