mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
Merge pull request #7319 from ouchihao/codex/bug-dialog-scroll-lock
fix(dialog): keep body locked while another dialog is open
This commit is contained in:
@@ -44,6 +44,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
let dialogIdCounter = 0
|
||||
const openDialogs = new Set<string>()
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -115,6 +116,12 @@ const handleEscape = (event: KeyboardEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
const updateScrollLock = (isOpen: boolean) => {
|
||||
if (isOpen) openDialogs.add(dialogId)
|
||||
else openDialogs.delete(dialogId)
|
||||
document.body.classList.toggle('modal-open', openDialogs.size > 0)
|
||||
}
|
||||
|
||||
// Prevent body scroll when modal is open and manage focus
|
||||
watch(
|
||||
() => props.show,
|
||||
@@ -123,7 +130,7 @@ watch(
|
||||
// 保存当前焦点元素
|
||||
previousActiveElement = document.activeElement as HTMLElement
|
||||
// 使用CSS类而不是直接操作style,更易于管理多个对话框
|
||||
document.body.classList.add('modal-open')
|
||||
updateScrollLock(true)
|
||||
|
||||
// 等待DOM更新后设置焦点到对话框
|
||||
await nextTick()
|
||||
@@ -137,7 +144,7 @@ watch(
|
||||
firstFocusable?.focus()
|
||||
}
|
||||
} else {
|
||||
document.body.classList.remove('modal-open')
|
||||
updateScrollLock(false)
|
||||
// 恢复之前的焦点
|
||||
if (previousActiveElement && typeof previousActiveElement.focus === 'function') {
|
||||
previousActiveElement.focus()
|
||||
@@ -155,6 +162,6 @@ onMounted(() => {
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
// 确保组件卸载时移除滚动锁定
|
||||
document.body.classList.remove('modal-open')
|
||||
updateScrollLock(false)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { enableAutoUnmount, mount } from '@vue/test-utils'
|
||||
import BaseDialog from '../BaseDialog.vue'
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
function dialog(show = true) {
|
||||
return mount(BaseDialog, { props: { show, title: 'Details' }, global: { stubs: { Icon: true } } })
|
||||
}
|
||||
|
||||
const locked = () => document.body.classList.contains('modal-open')
|
||||
|
||||
describe('dialog body scroll lock', () => {
|
||||
it('keeps the body locked when a hidden sibling mounts or unmounts', () => {
|
||||
dialog()
|
||||
const hidden = dialog(false)
|
||||
expect(locked()).toBe(true)
|
||||
hidden.unmount()
|
||||
expect(locked()).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the lock until the last visible dialog closes', async () => {
|
||||
const first = dialog()
|
||||
const second = dialog()
|
||||
await second.setProps({ show: false })
|
||||
expect(locked()).toBe(true)
|
||||
await first.setProps({ show: false })
|
||||
expect(locked()).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps the lock until the last visible dialog unmounts', () => {
|
||||
const first = dialog()
|
||||
const second = dialog()
|
||||
second.unmount()
|
||||
expect(locked()).toBe(true)
|
||||
first.unmount()
|
||||
expect(locked()).toBe(false)
|
||||
})
|
||||
|
||||
it('releases the lock after repeated open and close cycles', async () => {
|
||||
const wrapper = dialog(false)
|
||||
expect(locked()).toBe(false)
|
||||
await wrapper.setProps({ show: true })
|
||||
expect(locked()).toBe(true)
|
||||
await wrapper.setProps({ show: false })
|
||||
expect(locked()).toBe(false)
|
||||
await wrapper.setProps({ show: true })
|
||||
wrapper.unmount()
|
||||
expect(locked()).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user