diff --git a/src/pages/Chat/ChatRoom.tsx b/src/pages/Chat/ChatRoom.tsx index 9471ba4..e390578 100644 --- a/src/pages/Chat/ChatRoom.tsx +++ b/src/pages/Chat/ChatRoom.tsx @@ -84,8 +84,6 @@ function ChatRoom() { useChat(Number(chatRoomId)); const [value, setValue] = useState(''); - useViewportHeightLock(); - const textareaRef = useRef(null); const baseTextareaHeightRef = useRef(0); const { scrollContainerRef, topRef, scrollToBottom } = useChatRoomScroll({ @@ -97,6 +95,8 @@ function ChatRoom() { isFetchingNextPage, }); + useViewportHeightLock(scrollContainerRef); + const currentRoom = chatRoomList.rooms.find((room) => room.roomId === Number(chatRoomId)); const isGroup = currentRoom?.chatType === 'GROUP'; diff --git a/src/utils/hooks/useViewportHeightLock.ts b/src/utils/hooks/useViewportHeightLock.ts index c2fb462..230e2f4 100644 --- a/src/utils/hooks/useViewportHeightLock.ts +++ b/src/utils/hooks/useViewportHeightLock.ts @@ -1,9 +1,9 @@ -import { useLayoutEffect } from 'react'; +import { useLayoutEffect, type RefObject } from 'react'; import { isTextInputElement } from '@/utils/ts/dom'; const SCROLL_RESET_TIMEOUT_MS = 180; -function useViewportHeightLock() { +function useViewportHeightLock(scrollContainerRef?: RefObject) { useLayoutEffect(() => { const root = document.documentElement; const body = document.body; @@ -64,7 +64,48 @@ function useViewportHeightLock() { } }; + let lastTouchClientY = 0; + + const handleTouchStart = (event: TouchEvent) => { + if (event.touches.length !== 1) return; + + lastTouchClientY = event.touches[0].clientY; + }; + + const handleTouchMove = (event: TouchEvent) => { + if (event.touches.length !== 1) return; + if (!isEditableFocused) return; + + const scrollContainer = scrollContainerRef?.current; + const target = event.target; + const targetElement = + target instanceof HTMLElement ? target : target instanceof Node ? target.parentElement : null; + const editableElement = targetElement?.closest('input, textarea, [contenteditable]'); + + if (!scrollContainer) return; + if (editableElement instanceof HTMLElement && isTextInputElement(editableElement)) return; + + if (!(target instanceof Node) || !scrollContainer.contains(target)) { + event.preventDefault(); + return; + } + + const currentTouchClientY = event.touches[0].clientY; + const deltaY = currentTouchClientY - lastTouchClientY; + const canScroll = scrollContainer.scrollHeight > scrollContainer.clientHeight; + const isAtTop = scrollContainer.scrollTop <= 0; + const isAtBottom = scrollContainer.scrollTop + scrollContainer.clientHeight >= scrollContainer.scrollHeight - 1; + + lastTouchClientY = currentTouchClientY; + + if (!canScroll || (deltaY > 0 && isAtTop) || (deltaY < 0 && isAtBottom)) { + event.preventDefault(); + } + }; + const handleWindowScroll = () => { + if (!isEditableFocused) return; + const currentScrollTop = Math.max( scrollingElement?.scrollTop ?? 0, root.scrollTop, @@ -88,6 +129,8 @@ function useViewportHeightLock() { window.addEventListener('focusin', handleFocusIn); window.addEventListener('focusout', handleFocusOut); window.addEventListener('scroll', handleWindowScroll, { passive: true }); + document.addEventListener('touchstart', handleTouchStart, { passive: true }); + document.addEventListener('touchmove', handleTouchMove, { passive: false }); window.visualViewport?.addEventListener('resize', handleViewportChange); window.visualViewport?.addEventListener('scroll', handleViewportChange); @@ -100,6 +143,8 @@ function useViewportHeightLock() { window.removeEventListener('focusin', handleFocusIn); window.removeEventListener('focusout', handleFocusOut); window.removeEventListener('scroll', handleWindowScroll); + document.removeEventListener('touchstart', handleTouchStart); + document.removeEventListener('touchmove', handleTouchMove); window.visualViewport?.removeEventListener('resize', handleViewportChange); window.visualViewport?.removeEventListener('scroll', handleViewportChange); @@ -109,7 +154,7 @@ function useViewportHeightLock() { body.style.height = prevBodyHeight; root.style.height = prevRootHeight; }; - }, []); + }, [scrollContainerRef]); } export default useViewportHeightLock;