v3: modals stop shifting the page — scroll lock moves to <html> and holds the scrollbar's gutter only while locked (and only if a bar was showing), shared by Modal and ConfirmModal with stacked-lock counting; scrollbars take a theme-tinted thumb, inner scrollers go thin and contain their overscroll
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 40s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 40s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 0.75rem 1rem;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
.md pre code {
|
||||
@@ -1412,6 +1413,8 @@ a.global-player-title:hover {
|
||||
padding: 0 0.5rem 0.5rem;
|
||||
max-height: 40vh;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.global-player-track > button {
|
||||
display: flex;
|
||||
@@ -2893,6 +2896,8 @@ body.has-player .chat-fab {
|
||||
trigger's viewport rect, flipping above it when there isn't room below */
|
||||
min-width: 180px;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: thin;
|
||||
background: var(--color-surface);
|
||||
border: var(--border-width) solid var(--color-border-subtle);
|
||||
border-radius: var(--radius-xl);
|
||||
@@ -3936,6 +3941,8 @@ body.has-player .chat-fab {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: thin;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { useScrollLock } from "../hooks/useScrollLock";
|
||||
|
||||
interface ConfirmModalProps {
|
||||
message: string;
|
||||
@@ -14,6 +15,7 @@ export function ConfirmModal(
|
||||
{ message, confirmLabel, onConfirm, onCancel }: ConfirmModalProps,
|
||||
) {
|
||||
const label = confirmLabel ?? t`Delete`;
|
||||
useScrollLock();
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type ReactNode, useCallback, useEffect, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { useScrollLock } from "../hooks/useScrollLock";
|
||||
|
||||
interface ModalProps {
|
||||
title: string;
|
||||
@@ -25,12 +26,7 @@ export function Modal(
|
||||
onClose();
|
||||
}, [onBeforeClose, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, []);
|
||||
useScrollLock();
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
|
||||
31
src/hooks/useScrollLock.ts
Normal file
31
src/hooks/useScrollLock.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
let locks = 0;
|
||||
|
||||
/**
|
||||
* Freezes the page's scroll while the calling component is mounted — for
|
||||
* modals, whose backdrop shouldn't let the feed slide underneath.
|
||||
*
|
||||
* Hiding the viewport's overflow drops its scrollbar, which would widen the
|
||||
* page and shift everything behind the backdrop. If a bar is showing, its
|
||||
* gutter is held for the duration of the lock instead, so the layout stays
|
||||
* put; no gutter is reserved the rest of the time. Locks are counted so a
|
||||
* confirm stacked on a modal doesn't release the page when it closes first.
|
||||
*/
|
||||
export function useScrollLock() {
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (locks++ === 0) {
|
||||
if (window.innerWidth > root.clientWidth) {
|
||||
root.style.scrollbarGutter = "stable";
|
||||
}
|
||||
root.style.overflowY = "hidden";
|
||||
}
|
||||
return () => {
|
||||
if (--locks === 0) {
|
||||
root.style.overflowY = "";
|
||||
root.style.scrollbarGutter = "";
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
@@ -4,11 +4,17 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow-x: clip;
|
||||
/* Inherited by every scroller: a theme-tinted thumb on a clear track
|
||||
instead of the stock grey bar. */
|
||||
scrollbar-color: color-mix(in srgb, var(--color-text-muted) 45%, transparent)
|
||||
transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
overflow-x: clip;
|
||||
scrollbar-gutter: stable;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
Reference in New Issue
Block a user