270 lines
7.9 KiB
JavaScript
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;
|