Files
shoptime/com.twin.app.shoptime/src/views/DetailPanel/components/TScroller/TScrollerDetail.jsx
junghoon86.park 8325070138 [Tscrolldetail] 상세에서 오른쪽 스크롤 포커스 가지않도록 변경.
- focusableScrollbar를 false 로 강제로 줌.
2025-12-16 12:22:30 +09:00

270 lines
7.9 KiB
JavaScript

import React, {
forwardRef,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import {
off,
on,
} from '@enact/core/dispatcher';
import { Job } from '@enact/core/util';
import Scroller from '@enact/sandstone/Scroller';
import Spotlight from '@enact/spotlight';
import AutoScrollAreaDetail, {
POSITION,
} from '../AutoScrollAreaDetail/AutoScrollAreaDetail';
import css from './TScrollerDetail.module.less';
/**
* DetailPanel 전용 TScroller - 커스텀 스크롤바 구현
* onScroll* event can't use Callback dependency
*/
const TScrollerDetail = forwardRef(
(
{
className,
children,
verticalScrollbar = 'hidden',
focusableScrollbar = false,
direction = 'vertical',
horizontalScrollbar = 'hidden',
scrollMode,
onScrollStart,
onScrollStop,
onScroll,
noScrollByWheel = false,
cbScrollTo,
autoScroll = direction === 'horizontal',
setScrollVerticalPos,
setCheckScrollPosition,
cursorVisible = true, // prop으로 받되 기본값은 true (대부분 cursor 표시)
...rest
},
ref
) => {
// Redux 구독 제거 - cursorVisible은 prop으로만 받음
const isScrolling = useRef(false);
const scrollPosition = useRef('top');
const thumbElementRef = useRef(null); // 스크롤바 thumb 요소 저장
const scrollToRef = useRef(null);
const scrollHorizontalPos = useRef(0);
const scrollVerticalPos = useRef(0);
const actualScrollerElement = useRef(null); // 실제 스크롤 DOM 요소
const [isMounted, setIsMounted] = useState(false);
// ref를 내부 Scroller 요소에 연결
useEffect(() => {
if (ref && isMounted) {
// DOM에서 Scroller 요소 찾기
let scrollerElement = document.querySelector(`.${css.tScroller}`);
if (!scrollerElement) {
// 다른 방법으로 찾기
scrollerElement = document.querySelector('[data-spotlight-container="true"]');
}
if (!scrollerElement) {
// 스크롤 가능한 요소 찾기
scrollerElement = document.querySelector('[style*="overflow"]');
}
if (scrollerElement) {
// ref가 함수인 경우와 객체인 경우를 모두 처리
if (typeof ref === 'function') {
ref(scrollerElement);
} else if (ref && ref.current !== undefined) {
ref.current = scrollerElement;
}
actualScrollerElement.current = scrollerElement; // 실제 스크롤 요소 저장
}
}
}, [ref, isMounted]);
// 스크롤 제어 메서드 추가
const scrollToElement = useCallback((element) => {
if (actualScrollerElement.current && element) {
const scrollerRect = actualScrollerElement.current.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
const relativeTop = elementRect.top - scrollerRect.top;
const scrollTop = actualScrollerElement.current.scrollTop + relativeTop - 20;
actualScrollerElement.current.scrollTo({
top: scrollTop,
behavior: 'smooth',
});
}
}, []);
useEffect(() => {
setIsMounted(true);
return () => setIsMounted(false);
}, []);
const _onScrollStart = useCallback(
(e) => {
// 스크롤 시작 시 현재 포커스된 요소가 thumb인지 확인하고 저장
const currentFocused = Spotlight.getCurrent();
if (currentFocused && currentFocused.getAttribute('aria-label')?.includes('scroll')) {
thumbElementRef.current = currentFocused;
}
if (onScrollStart) {
onScrollStart(e);
}
isScrolling.current = true;
},
[onScrollStart]
);
const _onScrollStop = useCallback(
(e) => {
if (onScrollStop) {
onScrollStop(e);
}
isScrolling.current = false;
// 이전 스크롤 위치 저장
const prevScrollLeft = scrollHorizontalPos.current;
const prevScrollTop = scrollVerticalPos.current;
const prevPosition = scrollPosition.current;
// 현재 스크롤 위치 업데이트
scrollHorizontalPos.current = e.scrollLeft;
scrollVerticalPos.current = e.scrollTop;
// 스크롤 포지션 판단
let newPosition = 'middle';
if (e.reachedEdgeInfo) {
if (e.reachedEdgeInfo.top) {
newPosition = 'top';
} else if (e.reachedEdgeInfo.bottom) {
newPosition = 'bottom';
} else if (e.reachedEdgeInfo.left) {
newPosition = 'left';
} else if (e.reachedEdgeInfo.right) {
newPosition = 'right';
}
}
scrollPosition.current = newPosition;
// 값이 실제로 변경되었을 때만 콜백 호출 (불필요한 부모 재렌더링 방지)
if (setScrollVerticalPos && prevScrollTop !== scrollVerticalPos.current) {
setScrollVerticalPos(scrollVerticalPos.current);
}
if (setCheckScrollPosition && prevPosition !== scrollPosition.current) {
setCheckScrollPosition(scrollPosition.current);
}
// 스크롤 완료 후 thumb으로 포커스 복구
if (thumbElementRef.current) {
setTimeout(() => {
Spotlight.focus(thumbElementRef.current);
}, 50);
}
},
[onScrollStop]
);
const _onScroll = useCallback(
(ev) => {
if (onScroll) {
onScroll(ev);
}
},
[onScroll]
);
const _cbScrollTo = useCallback(
(ref) => {
if (cbScrollTo) {
cbScrollTo(ref);
}
scrollToRef.current = ref;
},
[cbScrollTo]
);
const relevantPositions = useMemo(() => {
switch (direction) {
case 'horizontal':
return ['left', 'right'];
case 'vertical':
return ['top', 'bottom'];
default:
return [];
}
}, [direction]);
return (
<div className={classNames(className ? className : null, css.scrollerContainer)}>
<Scroller
cbScrollTo={_cbScrollTo}
onScrollStart={_onScrollStart}
onScrollStop={_onScrollStop}
onScroll={_onScroll}
scrollMode={scrollMode || 'translate'}
// focusableScrollbar={focusableScrollbar}
focusableScrollbar={false}
className={classNames(isMounted && css.tScroller, noScrollByWheel && css.preventScroll)}
direction={direction}
horizontalScrollbar={horizontalScrollbar}
verticalScrollbar={verticalScrollbar}
overscrollEffectOn={{
arrowKey: false,
drag: false,
pageKey: false,
track: false,
wheel: false,
}}
noScrollByWheel={noScrollByWheel}
noScrollByDrag
// rest props에서 ref만 제외하고 전달
{...(rest.ref ? { ...rest, ref: undefined } : rest)}
>
{children}
</Scroller>
{autoScroll &&
relevantPositions.map((pos) => (
<AutoScrollAreaDetail
key={pos}
position={POSITION[pos]}
autoScroll={autoScroll}
scrollHorizontalPos={scrollHorizontalPos}
scrollVerticalPos={scrollVerticalPos}
scrollToRef={scrollToRef}
scrollPosition={scrollPosition}
direction={direction}
cursorVisible={cursorVisible}
/>
))}
</div>
);
}
);
// TScrollerDetail에 메서드 노출
TScrollerDetail.scrollToElement = (element) => {
// 이 메서드는 ref를 통해 접근할 수 있도록 구현
};
// displayName을 명확하게 설정
TScrollerDetail.displayName = 'TScrollerDetail';
// forwardRef를 사용하는 컴포넌트임을 명시
export default TScrollerDetail;