fix: lock body scroll when any modal or drawer is open

Add useLockBodyScroll hook (sets overflow:hidden on body, restores on
unmount) and apply it to ColorPicker, AdminColorFilter, WelcomeModal,
and GuidedTour. CartDrawer uses an inline effect keyed on drawerOpen
since it is always mounted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chris 2026-04-16 09:12:07 -04:00
parent e95ec68931
commit 6865d2d437
6 changed files with 26 additions and 0 deletions

View File

@ -2,6 +2,7 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { BASE } from '@/lib/basepath' import { BASE } from '@/lib/basepath'
import { useLockBodyScroll } from '@/lib/useLockBodyScroll'
interface ColorEntry { interface ColorEntry {
name: string name: string
@ -24,6 +25,7 @@ interface Props {
} }
export default function AdminColorFilter({ disabledColors, onSave, onClose }: Props) { export default function AdminColorFilter({ disabledColors, onSave, onClose }: Props) {
useLockBodyScroll()
const [families, setFamilies] = useState<ColorFamily[]>([]) const [families, setFamilies] = useState<ColorFamily[]>([])
const [disabled, setDisabled] = useState<Set<string>>(() => new Set(disabledColors)) const [disabled, setDisabled] = useState<Set<string>>(() => new Set(disabledColors))
const [openFamily, setOpenFamily] = useState<string | null>(null) const [openFamily, setOpenFamily] = useState<string | null>(null)

View File

@ -53,6 +53,13 @@ const STEP_ORDER: Step[] = ['cart', 'delivery', 'info', 'payment']
export default function CartDrawer() { export default function CartDrawer() {
const { entries, drawerOpen, closeDrawer, removeEntry, updateQuantity, clearCart, totalItems } = useCart() const { entries, drawerOpen, closeDrawer, removeEntry, updateQuantity, clearCart, totalItems } = useCart()
useEffect(() => {
if (!drawerOpen) return
const prev = document.body.style.overflow
document.body.style.overflow = 'hidden'
return () => { document.body.style.overflow = prev }
}, [drawerOpen])
const [editingEntry, setEditingEntry] = useState<CartEntry | null>(null) const [editingEntry, setEditingEntry] = useState<CartEntry | null>(null)
const [step, setStep] = useState<Step>('cart') const [step, setStep] = useState<Step>('cart')

View File

@ -6,6 +6,7 @@ import { useCart } from '@/context/CartContext'
import { BASE } from '@/lib/basepath' import { BASE } from '@/lib/basepath'
import type { CartEntry } from '@/context/CartContext' import type { CartEntry } from '@/context/CartContext'
import { fmt } from '@/lib/format' import { fmt } from '@/lib/format'
import { useLockBodyScroll } from '@/lib/useLockBodyScroll'
interface ColorEntry { interface ColorEntry {
name: string name: string
@ -29,6 +30,7 @@ interface Props {
} }
export default function ColorPicker({ product, maxColors, onClose, editingEntry }: Props) { export default function ColorPicker({ product, maxColors, onClose, editingEntry }: Props) {
useLockBodyScroll()
const { addToCart, updateEntry } = useCart() const { addToCart, updateEntry } = useCart()
const [families, setFamilies] = useState<ColorFamily[]>([]) const [families, setFamilies] = useState<ColorFamily[]>([])
const [openFamily, setOpenFamily] = useState<string | null>(null) const [openFamily, setOpenFamily] = useState<string | null>(null)

View File

@ -1,6 +1,7 @@
'use client' 'use client'
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { useLockBodyScroll } from '@/lib/useLockBodyScroll'
interface TourStep { interface TourStep {
target: string | null // CSS selector, or null = centered modal target: string | null // CSS selector, or null = centered modal
@ -62,6 +63,7 @@ interface Props {
} }
export default function GuidedTour({ onDone, onStart }: Props) { export default function GuidedTour({ onDone, onStart }: Props) {
useLockBodyScroll()
const [step, setStep] = useState(0) const [step, setStep] = useState(0)
const [targetRect, setTargetRect] = useState<DOMRect | null>(null) const [targetRect, setTargetRect] = useState<DOMRect | null>(null)

View File

@ -1,5 +1,7 @@
'use client' 'use client'
import { useLockBodyScroll } from '@/lib/useLockBodyScroll'
interface Props { interface Props {
onTour: () => void onTour: () => void
onDismiss: () => void onDismiss: () => void
@ -13,6 +15,7 @@ const HOW_IT_WORKS = [
] ]
export default function WelcomeModal({ onTour, onDismiss }: Props) { export default function WelcomeModal({ onTour, onDismiss }: Props) {
useLockBodyScroll()
return ( return (
<> <>
{/* Backdrop */} {/* Backdrop */}

View File

@ -0,0 +1,10 @@
import { useEffect } from 'react'
/** Locks body scroll while the calling component is mounted. */
export function useLockBodyScroll() {
useEffect(() => {
const prev = document.body.style.overflow
document.body.style.overflow = 'hidden'
return () => { document.body.style.overflow = prev }
}, [])
}