Fix nested UI scroll behavior

This commit is contained in:
Shu Yao 2026-05-12 10:55:39 +08:00
parent c85e1de2a7
commit f301108263
6 changed files with 94 additions and 7 deletions

View File

@ -7,10 +7,11 @@ const route = useRoute()
// Hide the sidebar on LaunchView, BatchRunView and WorkflowWorkbench
const showSidebar = computed(() => route.path !== '/launch' && route.path !== '/batch-run')
const isHomeRoute = computed(() => route.path === '/')
</script>
<template>
<div class="app-container">
<div class="app-container" :class="{ 'home-route': isHomeRoute }">
<Sidebar v-if="showSidebar" />
<main class="main-content">
<router-view />
@ -25,11 +26,23 @@ const showSidebar = computed(() => route.path !== '/launch' && route.path !== '/
min-height: 100vh;
}
.app-container.home-route {
height: 100dvh;
min-height: 100dvh;
overflow: hidden;
}
.main-content {
flex: 1;
min-height: 0;
background-color: white;
}
.home-route .main-content {
overflow: hidden;
background-color: #1a1a1a;
}
body {
margin: 0;
font-family: system-ui, sans-serif;

View File

@ -251,7 +251,16 @@
autocomplete="off"
/>
<Teleport to="body">
<div v-if="showDropdown && !isReadOnly" class="custom-select-dropdown" :style="dropdownStyle">
<div
v-if="showDropdown && !isReadOnly"
class="custom-select-dropdown"
data-local-scroll
:style="dropdownStyle"
@mousedown.prevent
@wheel="handleLocalScrollWheel"
@scroll.stop
@touchmove.stop
>
<div
v-if="!field.required"
class="custom-select-option"
@ -389,7 +398,10 @@
:id="`${modalId}-${field.name}`"
:value="formData[field.name]"
@input="onInput($event.target.value)"
@wheel="handleLocalScrollWheel"
@scroll.stop
class="form-textarea"
data-local-scroll
rows="4"
:readonly="isReadOnly"
:class="{'input-readonly': isReadOnly}"
@ -771,6 +783,34 @@ const onFilterInput = (event) => {
showDropdown.value = true
}
const handleLocalScrollWheel = (event) => {
const target = event.currentTarget
if (!target) {
return
}
const isDropdown = target.classList.contains('custom-select-dropdown')
if (target.scrollHeight <= target.clientHeight) {
if (isDropdown) {
event.stopPropagation()
event.preventDefault()
}
return
}
const isScrollingUp = event.deltaY < 0
const isScrollingDown = event.deltaY > 0
const atTop = target.scrollTop <= 0
const atBottom = Math.ceil(target.scrollTop + target.clientHeight) >= target.scrollHeight
event.stopPropagation()
if ((isScrollingUp && atTop) || (isScrollingDown && atBottom)) {
event.preventDefault()
}
}
const handleInputBlur = () => {
// Delay hiding dropdown to allow option selection
setTimeout(() => {
@ -897,6 +937,7 @@ const getSelectedLabel = () => {
box-sizing: border-box;
min-height: 80px;
resize: vertical;
overscroll-behavior: contain;
transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
}
@ -1565,6 +1606,7 @@ input:checked + .switch-slider:before {
z-index: 10000;
max-height: 200px;
overflow-y: auto;
overscroll-behavior: contain;
background-color: #1e1e1e;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;

View File

@ -14,7 +14,7 @@
</div>
<button class="close-button" @click="closeModal(modal.id)">×</button>
</div>
<div class="modal-body">
<div class="modal-body" data-local-scroll @wheel="handleLocalScrollWheel">
<div
v-for="field in getMainDisplayFields(modal)"
:key="field.name + '-' + (modal.formData[field.name]?.type || '')"
@ -128,7 +128,7 @@
<div class="modal-header">
<button class="close-button" @click="closeVarModal">×</button>
</div>
<div class="modal-body">
<div class="modal-body" data-local-scroll @wheel="handleLocalScrollWheel">
<div v-if="varFormError" class="submit-error">
{{ varFormError }}
</div>
@ -169,7 +169,7 @@
<h3 class="modal-title">{{ editingListItemIndex !== null ? t('form_generator.edit_entry') : t('form_generator.add_entry') }}</h3>
<button class="close-button" @click="closeListItemModal">×</button>
</div>
<div class="modal-body">
<div class="modal-body" data-local-scroll @wheel="handleLocalScrollWheel">
<div v-if="listItemFormError" class="submit-error">
{{ listItemFormError }}
</div>
@ -1672,6 +1672,29 @@ const submitForm = async (modalId) => {
}
// Press Enter to submit topmost modal, Esc to close
const handleLocalScrollWheel = (event) => {
const target = event.currentTarget
if (!target) {
return
}
event.stopPropagation()
if (target.scrollHeight <= target.clientHeight) {
event.preventDefault()
return
}
const isScrollingUp = event.deltaY < 0
const isScrollingDown = event.deltaY > 0
const atTop = target.scrollTop <= 0
const atBottom = Math.ceil(target.scrollTop + target.clientHeight) >= target.scrollHeight
if ((isScrollingUp && atTop) || (isScrollingDown && atBottom)) {
event.preventDefault()
}
}
const handleKeystrokes = (event) => {
if (event.key === 'Enter') {
// Do not intercept Enter inside textarea to allow newlines
@ -2140,6 +2163,7 @@ defineExpose({
padding: 15px 20px 30px 15px;
max-height: none;
overflow-y: auto;
overscroll-behavior: contain;
border-top: 1px solid rgba(255, 255, 255, 0.04);
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
scrollbar-width: none;

View File

@ -213,7 +213,11 @@ const calculatePosition = () => {
}
// Listen to scroll and zoom events to dismiss tooltip
const handleScroll = () => {
const handleScroll = (event) => {
if (event.target instanceof Element && event.target.closest('[data-local-scroll]')) {
return
}
if (isVisible.value && !keyboardActive.value) {
hideTooltip()
}

View File

@ -48,6 +48,10 @@ const isWorkflowsActive = computed(() => route.path.startsWith('/workflows'))
let lastScrollY = 0
const handleScroll = (e) => {
if (e.target instanceof Element && e.target.closest('[data-local-scroll]')) {
return;
}
const currentScrollY = e.target.scrollTop || window.scrollY || 0;
// Minimize small scroll jitters
if (Math.abs(currentScrollY - lastScrollY) < 5) return;

View File

@ -68,7 +68,7 @@ const cubes = Array.from({ length: 80 }, (_, i) => ({
<style scoped>
.home-view {
width: 100%;
min-height: calc(100vh - 55px); /* Match sidebar height to avoid bottom gap */
height: 100%;
background-color: #1a1a1a;
display: flex;
justify-content: center;