Merge pull request #618 from zxrys/fix/ui-scrolling-issue-fix

Fix nested UI scroll behavior
This commit is contained in:
Yufan Dang 2026-05-12 21:23:48 +08:00 committed by GitHub
commit b23950d035
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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 // Hide the sidebar on LaunchView, BatchRunView and WorkflowWorkbench
const showSidebar = computed(() => route.path !== '/launch' && route.path !== '/batch-run') const showSidebar = computed(() => route.path !== '/launch' && route.path !== '/batch-run')
const isHomeRoute = computed(() => route.path === '/')
</script> </script>
<template> <template>
<div class="app-container"> <div class="app-container" :class="{ 'home-route': isHomeRoute }">
<Sidebar v-if="showSidebar" /> <Sidebar v-if="showSidebar" />
<main class="main-content"> <main class="main-content">
<router-view /> <router-view />
@ -25,11 +26,23 @@ const showSidebar = computed(() => route.path !== '/launch' && route.path !== '/
min-height: 100vh; min-height: 100vh;
} }
.app-container.home-route {
height: 100dvh;
min-height: 100dvh;
overflow: hidden;
}
.main-content { .main-content {
flex: 1; flex: 1;
min-height: 0;
background-color: white; background-color: white;
} }
.home-route .main-content {
overflow: hidden;
background-color: #1a1a1a;
}
body { body {
margin: 0; margin: 0;
font-family: system-ui, sans-serif; font-family: system-ui, sans-serif;

View File

@ -251,7 +251,16 @@
autocomplete="off" autocomplete="off"
/> />
<Teleport to="body"> <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 <div
v-if="!field.required" v-if="!field.required"
class="custom-select-option" class="custom-select-option"
@ -389,7 +398,10 @@
:id="`${modalId}-${field.name}`" :id="`${modalId}-${field.name}`"
:value="formData[field.name]" :value="formData[field.name]"
@input="onInput($event.target.value)" @input="onInput($event.target.value)"
@wheel="handleLocalScrollWheel"
@scroll.stop
class="form-textarea" class="form-textarea"
data-local-scroll
rows="4" rows="4"
:readonly="isReadOnly" :readonly="isReadOnly"
:class="{'input-readonly': isReadOnly}" :class="{'input-readonly': isReadOnly}"
@ -771,6 +783,34 @@ const onFilterInput = (event) => {
showDropdown.value = true 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 = () => { const handleInputBlur = () => {
// Delay hiding dropdown to allow option selection // Delay hiding dropdown to allow option selection
setTimeout(() => { setTimeout(() => {
@ -897,6 +937,7 @@ const getSelectedLabel = () => {
box-sizing: border-box; box-sizing: border-box;
min-height: 80px; min-height: 80px;
resize: vertical; resize: vertical;
overscroll-behavior: contain;
transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease; 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; z-index: 10000;
max-height: 200px; max-height: 200px;
overflow-y: auto; overflow-y: auto;
overscroll-behavior: contain;
background-color: #1e1e1e; background-color: #1e1e1e;
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px; border-radius: 8px;

View File

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

View File

@ -213,7 +213,11 @@ const calculatePosition = () => {
} }
// Listen to scroll and zoom events to dismiss tooltip // 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) { if (isVisible.value && !keyboardActive.value) {
hideTooltip() hideTooltip()
} }

View File

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

View File

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