From 9d5c8d961c61fcffa9d07bb99fa4aae7b9155c07 Mon Sep 17 00:00:00 2001 From: "chihao.ou" Date: Fri, 18 Sep 2026 16:37:03 +0800 Subject: [PATCH] fix(dialog): keep body locked while another dialog is open --- frontend/src/components/common/BaseDialog.vue | 13 +++-- .../__tests__/BaseDialog.scrollLock.spec.ts | 51 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/common/__tests__/BaseDialog.scrollLock.spec.ts diff --git a/frontend/src/components/common/BaseDialog.vue b/frontend/src/components/common/BaseDialog.vue index e24c6fc226..ba0fe6a051 100644 --- a/frontend/src/components/common/BaseDialog.vue +++ b/frontend/src/components/common/BaseDialog.vue @@ -44,6 +44,7 @@ diff --git a/frontend/src/components/common/__tests__/BaseDialog.scrollLock.spec.ts b/frontend/src/components/common/__tests__/BaseDialog.scrollLock.spec.ts new file mode 100644 index 0000000000..3e4182f3c2 --- /dev/null +++ b/frontend/src/components/common/__tests__/BaseDialog.scrollLock.spec.ts @@ -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) + }) +})