From ff0203ff949aaa98b5375a35f939959ebe18e7d0 Mon Sep 17 00:00:00 2001 From: "fengzhihao.infeng" Date: Thu, 20 Aug 2026 17:56:28 +0800 Subject: [PATCH] fix: measure scrollbar compensation --- src/ViewerCore.tsx | 6 ++-- src/__tests__/viewer.test.tsx | 65 +++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/ViewerCore.tsx b/src/ViewerCore.tsx index 47db37b2..b01364e8 100644 --- a/src/ViewerCore.tsx +++ b/src/ViewerCore.tsx @@ -190,9 +190,11 @@ export default (props: ViewerProps) => { overflowY: document.body.style.overflowY, paddingRight: document.body.style.paddingRight, }; + const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; document.body.style.overflow = 'hidden'; - if (document.body.scrollHeight > document.body.clientHeight) { - document.body.style.paddingRight = '15px'; + if (document.body.scrollHeight > document.body.clientHeight && scrollbarWidth > 0) { + const bodyPaddingRight = parseFloat(window.getComputedStyle(document.body).paddingRight) || 0; + document.body.style.paddingRight = `${bodyPaddingRight + scrollbarWidth}px`; } return () => { document.body.style.overflow = originalBodyStyle.overflow; diff --git a/src/__tests__/viewer.test.tsx b/src/__tests__/viewer.test.tsx index 84515cdd..fd7446dd 100644 --- a/src/__tests__/viewer.test.tsx +++ b/src/__tests__/viewer.test.tsx @@ -13,6 +13,14 @@ function $$(className) { return document.body.querySelectorAll(className); } +function restoreProperty(target, property, descriptor) { + if (descriptor) { + Object.defineProperty(target, property, descriptor); + } else { + delete target[property]; + } +} + interface ViewerTesterProps { hasContainer?: boolean; onChangeImages?: () => ViewerProps['images']; @@ -587,6 +595,63 @@ describe('Viewer', () => { document.body.style.paddingRight = ''; }); + it('preserves body padding for overlay scrollbars', () => { + const originalPaddingRight = document.body.style.paddingRight; + const originalInnerWidth = Object.getOwnPropertyDescriptor(window, 'innerWidth'); + const originalClientWidth = Object.getOwnPropertyDescriptor(document.documentElement, 'clientWidth'); + const originalScrollHeight = Object.getOwnPropertyDescriptor(document.body, 'scrollHeight'); + const originalClientHeight = Object.getOwnPropertyDescriptor(document.body, 'clientHeight'); + Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1000 }); + Object.defineProperty(document.documentElement, 'clientWidth', { configurable: true, value: 1000 }); + Object.defineProperty(document.body, 'scrollHeight', { configurable: true, value: 1000 }); + Object.defineProperty(document.body, 'clientHeight', { configurable: true, value: 500 }); + let overlayViewer = null; + + try { + document.body.style.paddingRight = '7px'; + overlayViewer = mount(); + + expect(document.body.style.paddingRight).toBe('7px'); + } finally { + if (overlayViewer) { + overlayViewer.unmount(); + } + document.body.style.paddingRight = originalPaddingRight; + restoreProperty(window, 'innerWidth', originalInnerWidth); + restoreProperty(document.documentElement, 'clientWidth', originalClientWidth); + restoreProperty(document.body, 'scrollHeight', originalScrollHeight); + restoreProperty(document.body, 'clientHeight', originalClientHeight); + } + }); + + it('adds the measured scrollbar width to existing body padding', () => { + const originalInnerWidth = Object.getOwnPropertyDescriptor(window, 'innerWidth'); + const originalClientWidth = Object.getOwnPropertyDescriptor(document.documentElement, 'clientWidth'); + const originalScrollHeight = Object.getOwnPropertyDescriptor(document.body, 'scrollHeight'); + const originalClientHeight = Object.getOwnPropertyDescriptor(document.body, 'clientHeight'); + Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1000 }); + Object.defineProperty(document.documentElement, 'clientWidth', { configurable: true, value: 980 }); + Object.defineProperty(document.body, 'scrollHeight', { configurable: true, value: 1000 }); + Object.defineProperty(document.body, 'clientHeight', { configurable: true, value: 500 }); + let scrollbarViewer = null; + + try { + document.body.style.paddingRight = '7px'; + scrollbarViewer = mount(); + + expect(document.body.style.paddingRight).toBe('27px'); + } finally { + if (scrollbarViewer) { + scrollbarViewer.unmount(); + } + document.body.style.paddingRight = ''; + restoreProperty(window, 'innerWidth', originalInnerWidth); + restoreProperty(document.documentElement, 'clientWidth', originalClientWidth); + restoreProperty(document.body, 'scrollHeight', originalScrollHeight); + restoreProperty(document.body, 'clientHeight', originalClientHeight); + } + }); + it('reset image', () => { viewerHelper.new(); viewerHelper.open();