Skip to content

Commit 5c9b910

Browse files
committed
feat: improve menu option scroll into viewport
1 parent 6274604 commit 5c9b910

File tree

2 files changed

+26
-11
lines changed

2 files changed

+26
-11
lines changed

src/MenuOption.vue

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
<script setup lang="ts">
2-
import { nextTick, ref, watch } from "vue";
2+
import { ref, watch } from "vue";
33
44
const props = defineProps<{
5+
menu: HTMLDivElement | null;
6+
index: number;
57
isFocused: boolean;
68
isSelected: boolean;
79
}>();
@@ -15,15 +17,24 @@ const option = ref<HTMLButtonElement | null>(null);
1517
// Scroll the focused option into view when it's out of the menu's viewport.
1618
watch(
1719
() => props.isFocused,
18-
async () => {
19-
if (props.isFocused) {
20-
// Use nextTick to wait for the next DOM render.
21-
await nextTick(() => {
22-
option.value?.parentElement?.scrollTo({
23-
top: option.value?.offsetTop - option.value?.parentElement?.offsetHeight + option.value?.offsetHeight,
24-
behavior: "instant",
25-
});
26-
});
20+
() => {
21+
if (props.isFocused && props.menu) {
22+
// Get child element with index
23+
const option = props.menu.children[props.index] as HTMLDivElement;
24+
25+
const optionTop = option.offsetTop;
26+
const optionBottom = optionTop + option.clientHeight;
27+
const menuScrollTop = props.menu.scrollTop;
28+
const menuHeight = props.menu.clientHeight;
29+
30+
if (optionTop < menuScrollTop) {
31+
// eslint-disable-next-line vue/no-mutating-props
32+
props.menu.scrollTop = optionTop;
33+
}
34+
else if (optionBottom > menuScrollTop + menuHeight) {
35+
// eslint-disable-next-line vue/no-mutating-props
36+
props.menu.scrollTop = optionBottom - menuHeight;
37+
}
2738
}
2839
},
2940
);

src/Select.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,9 @@ const selected = defineModel<string | string[]>({
9898
},
9999
});
100100
101-
const container = ref<HTMLElement | null>(null);
101+
const container = ref<HTMLDivElement | null>(null);
102102
const input = ref<HTMLInputElement | null>(null);
103+
const menu = ref<HTMLDivElement | null>(null);
103104
104105
const search = ref("");
105106
const menuOpen = ref(false);
@@ -384,6 +385,7 @@ onBeforeUnmount(() => {
384385
<Teleport :to="teleport" :disabled="!teleport">
385386
<div
386387
v-if="menuOpen"
388+
ref="menu"
387389
class="menu"
388390
role="listbox"
389391
:aria-label="aria?.labelledby"
@@ -400,6 +402,8 @@ onBeforeUnmount(() => {
400402
type="button"
401403
class="menu-option"
402404
:class="{ focused: focusedOption === i, selected: option.value === selected }"
405+
:menu="menu"
406+
:index="i"
403407
:is-focused="focusedOption === i"
404408
:is-selected="option.value === selected"
405409
@select="setOption(option.value)"

0 commit comments

Comments
 (0)