Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/pages/Chat/ChatRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,6 @@ function ChatRoom() {
useChat(Number(chatRoomId));
const [value, setValue] = useState('');

useViewportHeightLock();

const textareaRef = useRef<HTMLTextAreaElement>(null);
const baseTextareaHeightRef = useRef(0);
const { scrollContainerRef, topRef, scrollToBottom } = useChatRoomScroll({
Expand All @@ -97,6 +95,8 @@ function ChatRoom() {
isFetchingNextPage,
});

useViewportHeightLock(scrollContainerRef);

const currentRoom = chatRoomList.rooms.find((room) => room.roomId === Number(chatRoomId));

const isGroup = currentRoom?.chatType === 'GROUP';
Expand Down
51 changes: 48 additions & 3 deletions src/utils/hooks/useViewportHeightLock.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLElement | null>) {
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scrollContainerRef 타입이 RefObject<HTMLElement | null>로 되어 있는데, RefObject<T> 자체가 current: T | null 형태라서 | null이 중복됩니다. 타입을 RefObject<HTMLElement>(또는 구체적으로 RefObject<HTMLDivElement>)로 정리하면 호출부/IDE 타입 추론이 더 명확해집니다.

Copilot uses AI. Check for mistakes.
useLayoutEffect(() => {
const root = document.documentElement;
const body = document.body;
Expand Down Expand Up @@ -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;
}
Comment thread
ff1451 marked this conversation as resolved.

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,
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -109,7 +154,7 @@ function useViewportHeightLock() {
body.style.height = prevBodyHeight;
root.style.height = prevRootHeight;
};
}, []);
}, [scrollContainerRef]);
}

export default useViewportHeightLock;
Loading