fix(web): stop dialog scroll-lock from shifting the layout

Naive UI locks page scroll by setting overflow:hidden on <html>, which
drops the classic scrollbar width and still applies margin-right
compensation. That reintroduces horizontal jitter when n-dialog / n-modal
open or close, on top of the earlier short-to-long content shift.

Keep the vertical scrollbar track reserved with overflow-y:scroll
!important + scrollbar-gutter, ignore Naive's margin compensation, and
freeze background scroll via body position:fixed while the lock is active.
pull/728/head
HooperTrs 2 months ago
parent 2bb93f8c35
commit 894c23ac84

@ -3,12 +3,23 @@
--content-main: 620px;
}
// Reserve vertical scrollbar space so content doesn't jump horizontally
// when pages go from short (no bar) to tall (bar appears).
// Keep the document width stable when the vertical scrollbar appears/disappears.
//
// Two cases that used to shift the flex-centered layout + fixed sidebars:
// 1) short route → content loads and the classic bar appears
// 2) n-dialog / n-modal opens: Naive UI sets overflow:hidden on <html>,
// which removes the bar (and its own margin-right compensation still jitters
// with our centered/fixed layout)
//
// `overflow-y: scroll !important` keeps the scrollbar track always reserved so
// Naive's inline overflow:hidden cannot free that width. Background scrolling
// while a modal is open is blocked in utils/stableScrollbar.ts instead.
// `scrollbar-gutter: stable` covers engines that honor gutter without a forced bar.
// `margin-right: 0 !important` disables Naive's extra lock compensation.
html {
scrollbar-gutter: stable;
// Fallback for browsers without scrollbar-gutter support
overflow-y: scroll;
overflow-y: scroll !important;
margin-right: 0 !important;
}
.app-container {

@ -3,6 +3,7 @@ import { createPinia } from 'pinia';
import router from './router';
import App from './App.vue';
import '@/assets/css/main.less';
import { installStableScrollbar } from '@/utils/stableScrollbar';
import type { MessageApiInjection } from 'naive-ui/lib/message/src/MessageProvider';
@ -11,6 +12,8 @@ import 'vfonts/Lato.css';
// 等宽字体
import 'vfonts/FiraCode.css';
installStableScrollbar();
const pinia = createPinia();
createApp(App).use(router).use(pinia).mount('#app');

@ -0,0 +1,82 @@
/**
* Keep document width stable across:
* 1) short page long content (scrollbar appears)
* 2) n-dialog / n-modal scroll-lock (Naive UI sets overflow:hidden on <html>)
*
* Naive UI's useLockHtmlScroll removes the classic scrollbar and optionally
* adds margin-right compensation. That fights `scrollbar-gutter` / always-on
* overflow-y:scroll and shifts the flex-centered layout plus fixed sidebars
* (`left/right: calc(50% + ...)`).
*
* Strategy:
* - CSS keeps the vertical scrollbar track always present (see main.less)
* - When Naive marks <html> as locked, freeze body scroll with position:fixed
* without ever releasing the scrollbar width
*/
let installed = false;
export function installStableScrollbar() {
if (installed || typeof document === 'undefined') {
return;
}
installed = true;
const html = document.documentElement;
let frozen = false;
let scrollY = 0;
const isHtmlScrollLocked = () => {
const { overflow, overflowY } = html.style;
return overflow === 'hidden' || overflowY === 'hidden';
};
const freezeBackgroundScroll = () => {
if (frozen) {
return;
}
frozen = true;
scrollY = window.scrollY;
const body = document.body;
body.style.position = 'fixed';
body.style.top = `-${scrollY}px`;
body.style.left = '0';
body.style.right = '0';
body.style.width = '100%';
};
const unfreezeBackgroundScroll = () => {
if (!frozen) {
return;
}
frozen = false;
const body = document.body;
body.style.position = '';
body.style.top = '';
body.style.left = '';
body.style.right = '';
body.style.width = '';
window.scrollTo(0, scrollY);
};
const sync = () => {
// Drop Naive's margin compensation — scrollbar track already reserves width.
if (html.style.marginRight && html.style.marginRight !== '0px') {
html.style.marginRight = '0px';
}
if (isHtmlScrollLocked()) {
freezeBackgroundScroll();
} else {
unfreezeBackgroundScroll();
}
};
new MutationObserver(sync).observe(html, {
attributes: true,
attributeFilter: ['style'],
});
// In case lock styles are already present
sync();
}
Loading…
Cancel
Save