pagenator

This commit is contained in:
yonghyon
2024-06-03 22:52:15 +09:00
parent 76d1a813ee
commit 2aa04e489a
10 changed files with 241 additions and 200 deletions

View File

@@ -1,6 +1,6 @@
/**
* TVerticalPagenator
*
* checking data-wheel-point and data-spotlight-id
* @module TVerticalPagenator
*/
@@ -13,35 +13,35 @@ import * as HelperMethods from "../../utils/helperMethods";
import usePrevious from '../../hooks/usePrevious';
import Spotlight from '@enact/spotlight';
const DEBOUNCE_DELAY = 10;
const TVerticalPagenator = ({
className,
spotlightId,
defaultPageIndex=0,
defaultContainerId=null,
enableFocusAction=true,
children,
disabled,
pageSpotIds = [],
onPageChanged,
visible = "hidden",
scrollPositionRef,
onScrollStatusChanged,
onFocusedContainerId,
scrollbarVisible = "hidden",
cbChangePageRef,
scrollTop=false,
topMargin=0,
...rest
}) => {
const [pageIndex, setPageIndex] = useState(!defaultPageIndex ? 0: defaultPageIndex);
const [_defaultContainerId,_] = useState(defaultContainerId);
const pagenatorRef = useRef(null);
const pageIndexRef = usePrevious(pageIndex);
const isScrolling = useRef(false);
const scrollTopPositionRef = useRef(0);
const scrollTopPositionRef = useRef(-1);
const scrollTo = useRef(null);
const cursorVisible = useSelector((state) => state.common.appStatus.cursorVisible);
const cursorVisibleRef = usePrevious(cursorVisible);
const disabledRef = usePrevious(disabled);
const pageSpotIdsRef = usePrevious(pageSpotIds);
const topMarginRef = usePrevious(topMargin);
const initialRenderedRef = useRef(false);
const lastScrollOffset = useRef(0);
const onFocusedContainerIdRef = usePrevious(onFocusedContainerId);
const scrollStatus = useRef("top");
const lastEventTimeRef = useRef(0);
useEffect(() => {
if(spotlightId){
@@ -49,6 +49,39 @@ const TVerticalPagenator = ({
}
}, [spotlightId]);
const findCurrentWheelPoint = useCallback((node)=>{
if(pagenatorRef.current && pagenatorRef.current.contains(node)){
return node.closest('[data-wheel-point="true"]');
}else{
return null;
}
},[]);
const findNextWheelPoint = useCallback((ev)=>{
let matchedNode = null;
if(pagenatorRef.current){
const nodes = pagenatorRef.current.querySelectorAll(`[data-wheel-point="true"]`);
for(let i=0; i<nodes.length; i++){
if(scrollTopPositionRef.current < (nodes[i].offsetTop-topMarginRef.current)){
matchedNode = nodes[i];
break;
}
}
}
return matchedNode;
},[]);
const findPrevWheelPoint = useCallback((ev)=>{
let matchedNode = null;
if(pagenatorRef.current){
const nodes = pagenatorRef.current.querySelectorAll(`[data-wheel-point="true"]`);
for(let i=nodes.length-1; i>=0; i--){
if(scrollTopPositionRef.current > (nodes[i].offsetTop-topMarginRef.current)){
matchedNode = nodes[i];
break;
}
}
}
return matchedNode;
},[]);
const onFocusItem = useCallback((ev)=>{
if(!pagenatorRef.current){
return;
@@ -58,24 +91,33 @@ const TVerticalPagenator = ({
if (cursorVisibleRef.current) {
return;
}
pageSpotIds.forEach((element, index) => {
const pageNode = document.querySelector(`[data-spotlight-id="${element}"]`);
if(pageNode.contains(ev.target))
setPageIndex(index);
const node = findCurrentWheelPoint(ev.target);
if(node){
setTimeout(() => {
moveToNode(node, true, false);
}, 0);
}
);
},[pageSpotIds]);
},[moveToNode]);
const handleWheel = useCallback((ev) => {
const now = Date.now();
if (now - lastEventTimeRef.current < DEBOUNCE_DELAY) {
// <20><><EFBFBD><EFBFBD> <20>ð<EFBFBD> <20>̳<EFBFBD><CCB3><EFBFBD> <20>̺<EFBFBD>Ʈ<EFBFBD><C6AE> <20><><EFBFBD><EFBFBD>
return;
}
lastEventTimeRef.current = now;
if(pagenatorRef.current.contains(ev.target)){
ev.stopPropagation();
if(disabledRef.current){
return;
}
let newIndex = ev.deltaY > 0 ? pageIndexRef.current+1: pageIndexRef.current -1;
newIndex = Math.max(newIndex, 0);
newIndex = Math.min(newIndex, pageSpotIdsRef.current.length-1);
setPageIndex(newIndex);
const node = ev.deltaY > 0 ? findNextWheelPoint():findPrevWheelPoint();
if(node){
setTimeout(() => {
moveToNode(node, true, true);
}, 0);
}
}
}, []);
@@ -91,62 +133,90 @@ const TVerticalPagenator = ({
document.removeEventListener('wheel', handleWheel);
};
}, []);
const moveToPage = useCallback((index=0, animate=true, focus=enableFocusAction)=>{
if(pageSpotIds[index]){
const currentNode = document.querySelector(
`[data-spotlight-id="${pageSpotIds[index]}"]`
);
if(currentNode){
const distance = Math.round(currentNode.offsetTop - HelperMethods.scaleH(topMargin));
const moveToNode = useCallback((node, animate=true, focus=enableFocusAction)=>{
if(typeof node === 'string'){
node = pagenatorRef.current.querySelector(`[data-spotlight-id="${node}"]`);
}
if(node){
const distance = Math.round(node.offsetTop - HelperMethods.scaleH(topMarginRef.current));
const targetTop = distance >=0 ? distance:0;
scrollTo.current && scrollTo.current({ position:{x:0,y: targetTop}, animate: animate, focus: focus });
if(focus){
Spotlight.focus(currentNode);
const currentSpot = Spotlight.getCurrent();
if(scrollTopPositionRef.current === targetTop){
return;
}
scrollTopPositionRef.current = targetTop;
scrollTo.current && scrollTo.current({ position:{x:0,y: targetTop}, animate: animate, focus: focus || !currentSpot, align: 'top' });
if(focus || !currentSpot){
Spotlight.focus(node);
}
if(onFocusedContainerIdRef.current){
onFocusedContainerIdRef.current(node.getAttribute('data-spotlight-id'));
}
}
}
if(onPageChanged){
onPageChanged(index);
}
},[topMargin, pageSpotIds, onPageChanged, enableFocusAction]);
},[enableFocusAction]);
useEffect(() => {
let node = _defaultContainerId;
// console.log('yhcho TVerticalPagenator _defaultContainerId changed ', _defaultContainerId);
if(!_defaultContainerId){
node = pagenatorRef.current.querySelector(`[data-wheel-point="true"]`); //to top
}
if(!initialRenderedRef.current){
moveToPage(pageIndex, false);
moveToNode(node, false);
setTimeout(() => {
moveToPage(pageIndex, false);
moveToNode(node, false);
}, 100);
}else{
moveToPage(pageIndex, true);
moveToNode(node, true);
}
initialRenderedRef.current = true;
}, [pageIndex]);
}, [_defaultContainerId]);
useEffect(() => {
if(cbChangePageRef){
cbChangePageRef.current = (index, animate=false)=>{
moveToPage(index, animate);
setPageIndex(index ? index : 0);
cbChangePageRef.current = (_spotlightId, animate=false)=>{
let node = null;
if(!_spotlightId){ //to top
node = pagenatorRef.current.querySelector(`[data-wheel-point="true"]`);
}else{
node = pagenatorRef.current.querySelector(`[data-spotlight-id="${_spotlightId}"]`);
}
if(node){
setTimeout(() => {
moveToNode(node, animate);
}, 0);
}
};
}
}, [cbChangePageRef, moveToPage]);
}, [cbChangePageRef]);
const _onScrollStart = useCallback(() => {
isScrolling.current = true;
}, []);
const _onScrollStop = useCallback(() => {
const _onScrollStop = useCallback((ev) => {
isScrolling.current = false;
scrollTopPositionRef.current = ev.scrollTop;
}, []);
const _onScroll = useCallback((ev) => {
isScrolling.current = true;
scrollTopPositionRef.current = ev.scrollTop;
if(scrollPositionRef){
scrollPositionRef.current = ev.scrollTop;
let prevScrollStatus = scrollStatus.current;
const scroller = pagenatorRef.current?.childNodes?.[0];
if(scroller){
if(ev.scrollTop <=0){
scrollStatus.current = "top";
}else if(ev.scrollTop + scroller.offsetHeight >= scroller.scrollHeight){
scrollStatus.current = "end";
}else{
scrollStatus.current = "middle";
}
}, [scrollPositionRef]);
}
if(prevScrollStatus !== scrollStatus.current && onScrollStatusChanged){
onScrollStatusChanged(scrollStatus.current);
}
}, [onScrollStatusChanged]);
if(!spotlightId){
console.warn('TVerticalPagenator has no spotlightId');
return null;
@@ -157,10 +227,10 @@ const TVerticalPagenator = ({
id={spotlightId}
spotlightId={spotlightId}
className={classNames(css.verticalPagenator, className)}
focusableScrollbar={visible !== "hidden"}
focusableScrollbar={scrollbarVisible !== "hidden"}
noScrollByWheel={true}
noScrollByDrag={true}
verticalScrollbar={visible}
verticalScrollbar={scrollbarVisible}
onFocus={onFocusItem}
cbScrollTo={getScrollTo}
onScroll={_onScroll}

View File

@@ -113,7 +113,7 @@ const BestSeller = ({ order, scrollTopBody, spotlightId, handleItemFocus }) => {
}, [handleItemFocus]);
return (
<Container className={css.bestSellerWrap} style={orderStyle} spotlightId={spotlightId}>
<Container className={css.bestSellerWrap} style={orderStyle} spotlightId={spotlightId} data-wheel-point={true}>
<SectionTitle
title={$L("BEST SELLER")}
data-title-index="homeBestSellerTitle"

View File

@@ -193,7 +193,7 @@ export default function HomeBanner({
}, [handleItemFocus]);
return (
<Container className={css.container} spotlightId={spotlightId} style={orderStyle}>
<Container className={css.container} spotlightId={spotlightId} data-wheel-point={true} style={orderStyle} >
<div className={css.homeTemplateBox}>
<ContainerBasic
className={css.smallBox}

View File

@@ -84,7 +84,7 @@ const HomeOnSale = ({ order, scrollTopBody, handleItemFocus,spotlightId, ...rest
}, [handleItemFocus]);
return (
<Container {...rest} className={css.container} style={orderStyle} spotlightId={spotlightId}>
<Container {...rest} className={css.container} style={orderStyle} spotlightId={spotlightId} data-wheel-point={true}>
<div className={css.onSaleWrap}>
<SectionTitle
className={css.subTitle}

View File

@@ -51,11 +51,7 @@ export default function HomePanel({ isOnTop }) {
const { sendLogGNB } = useLogService();
const dispatch = useDispatch();
const [pageIndex, setPageIndex] = useState(0);
const pageIndexRef = usePrevious(pageIndex);
const spotYoffsetRef = useRef(0);
const cbChangePageRef = useRef(null);
const [btnActive, setBtnActive] = useState(true);
const [btnDisabled, setBtnDisabled] = useState(true);
const [arrowBottom, setArrowBottom] = useState(true);
useDebugKey({ isLandingPage: true });
@@ -94,7 +90,9 @@ export default function HomePanel({ isOnTop }) {
const webOSVersion = useSelector(
(state) => state.common.appStatus.webOSVersion
);
const [focusedContainerId, setFocusedContainerId] = useState(homeSpotlight?.focusedContainerId);
const focusedContainerIdRef = usePrevious(focusedContainerId);
const cbChangePageRef = useRef(null);
const selectTemplate = useMemo(()=>{
if (homeTopDisplayInfos) {
return homeTopDisplayInfos[0].shptmTmplCd;
@@ -155,7 +153,7 @@ export default function HomePanel({ isOnTop }) {
spotId = "banner03";
}
if(cbChangePageRef.current){
cbChangePageRef.current(0);
cbChangePageRef.current(0, true);
}
return Spotlight.focus(spotId);
}
@@ -207,37 +205,7 @@ export default function HomePanel({ isOnTop }) {
}, [dispatch, eventPopInfosData]);
useEffect(() => {
let timer;
if (homeSpotlight) {
if(cbChangePageRef.current){
cbChangePageRef.current(homeSpotlight.pageIndex);
}
timer = setTimeout(() => {
Spotlight.resume();
if(cbChangePageRef.current){ //call again after some seconds
cbChangePageRef.current(homeSpotlight.pageIndex);
}
if (homeSpotlight.currentCatCd) {
Spotlight.focus("spotlightId-" + homeSpotlight.currentCatCd);
}
if (homeSpotlight.currentSpot) {
Spotlight.focus(homeSpotlight.currentSpot);
}
setBtnActive(false);
setFirstSpot(true);
}, 300);
} else {
setBtnActive(false);
}
return () => {
clearTimeout(timer);
const c = Spotlight.getCurrent();
let targetSpotlightId = null;
let targetSpotlightCatcd = null;
@@ -260,7 +228,7 @@ export default function HomePanel({ isOnTop }) {
currentSpot: currentSpot,
currentCatCd: targetSpotlightCatcd,
currentCateName: targetSpotlightCateNm,
pageIndex: pageIndexRef.current,
focusedContainerId: focusedContainerIdRef.current,
},
})
);
@@ -273,10 +241,9 @@ export default function HomePanel({ isOnTop }) {
}
}, [firstSpot]);
const doSendLogGNB = useCallback((index)=>{
const doSendLogGNB = useCallback((containerId)=>{
let nowMenu = null;
if(pageSpotIds[index]){
switch(pageSpotIds[index]){
switch(containerId){
case TEMPLATE_CODE_CONF.TOP:
nowMenu=LOG_MENU.HOME_TOP;
break;
@@ -293,20 +260,19 @@ export default function HomePanel({ isOnTop }) {
nowMenu=LOG_MENU.HOME_BEST_SELLER;
break;
}
}
if(nowMenu){
sendLogGNB(nowMenu);
}
},[pageSpotIds, sendLogGNB]);
const handleItemFocus = useCallback((index)=> () => {
doSendLogGNB(index);
const handleItemFocus = useCallback((containerId)=> () => {
doSendLogGNB(containerId);
},
[doSendLogGNB]
);
useEffect(() => {
doSendLogGNB(pageIndex);
}, [pageIndex]);
doSendLogGNB(focusedContainerId);
}, [focusedContainerId]);
const renderPageItem = useCallback(()=>{
return (
@@ -322,7 +288,7 @@ export default function HomePanel({ isOnTop }) {
selectTemplate={selectTemplate}
firstSpot={firstSpot}
className={css.homeBannerWrap}
handleItemFocus={handleItemFocus(idx)}
handleItemFocus={handleItemFocus(el.shptmApphmDspyOptCd)}
/>
)
}
@@ -333,7 +299,7 @@ export default function HomePanel({ isOnTop }) {
spotlightId={el.shptmApphmDspyOptCd}
catCd={cateCd}
cateNm={cateNm}
handleItemFocus={handleItemFocus(idx)}
handleItemFocus={handleItemFocus(el.shptmApphmDspyOptCd)}
/>
)
}
@@ -342,7 +308,7 @@ export default function HomePanel({ isOnTop }) {
<HomeOnSale
key={el.shptmApphmDspyOptCd}
spotlightId={el.shptmApphmDspyOptCd}
handleItemFocus={handleItemFocus(idx)}
handleItemFocus={handleItemFocus(el.shptmApphmDspyOptCd)}
/>
)
}
@@ -351,7 +317,7 @@ export default function HomePanel({ isOnTop }) {
<PopularShow
key={el.shptmApphmDspyOptCd}
spotlightId={el.shptmApphmDspyOptCd}
handleItemFocus={handleItemFocus(idx)}
handleItemFocus={handleItemFocus(el.shptmApphmDspyOptCd)}
/>
)
}
@@ -360,7 +326,7 @@ export default function HomePanel({ isOnTop }) {
<BestSeller
key={el.shptmApphmDspyOptCd}
spotlightId={el.shptmApphmDspyOptCd}
handleItemFocus={handleItemFocus(idx)}
handleItemFocus={handleItemFocus(el.shptmApphmDspyOptCd)}
/>
)
}
@@ -373,21 +339,38 @@ export default function HomePanel({ isOnTop }) {
size={null}
type={TYPES.topButton}
spotlightId={"home-top-btn"}
spotlightDisabled={btnActive}
spotlightDisabled={btnDisabled}
aria-label="Move to Top, Button"
/>
</>
);
},[sortedHomeLayoutInfo, selectTemplate, cateCd,cateNm, handleItemFocus, handleTopButtonClick, btnActive]);
},[sortedHomeLayoutInfo, selectTemplate, cateCd,cateNm, handleItemFocus, handleTopButtonClick, btnDisabled]);
const onPageChanged = useCallback((index) => {
if(pageSpotIds.length <=index+1){
const onScrollStatusChanged = useCallback((status) => {
if(status === 'end'){
setArrowBottom(false);
}else{
setArrowBottom(true);
}
setPageIndex(index);
}, [pageSpotIds]);
}, []);
const onFocusedContainerId = useCallback((containerId) => {
setFocusedContainerId(containerId);
if (!firstSpot && homeSpotlight) {
setTimeout(() => {
Spotlight.resume();
setFirstSpot(true);
if (homeSpotlight.currentCatCd) {
Spotlight.focus("spotlightId-" + homeSpotlight.currentCatCd);
}
if (homeSpotlight?.currentSpot) {
Spotlight.focus(homeSpotlight.currentSpot);
}
setBtnDisabled(false);
}, 0);
} else if(!firstSpot){
setBtnDisabled(false);
}
}, [homeSpotlight, firstSpot]);
return (
<>
@@ -401,11 +384,10 @@ export default function HomePanel({ isOnTop }) {
<TVerticalPagenator
className={css.tVerticalPagenator}
spotlightId={'home_verticalPagenator'}
defaultPageIndex={homeSpotlight?.pageIndex ?? 0}
defaultContainerId={homeSpotlight?.focusedContainerId}
disabled={!isOnTop}
pageSpotIds={pageSpotIds}
onPageChanged={onPageChanged}
scrollPositionRef={spotYoffsetRef}
onScrollStatusChanged={onScrollStatusChanged}
onFocusedContainerId={onFocusedContainerId}
cbChangePageRef={cbChangePageRef}
topMargin={36}>
{renderPageItem()}

View File

@@ -13,7 +13,7 @@
}
}
.tButton {
margin-top: 60px;
margin-top: -20px;
}
}
.arrow {

View File

@@ -125,7 +125,7 @@ const PopularShow = ({
}, [handleItemFocus]);
return (
<Container className={css.popularShow} style={orderStyle} spotlightId={spotlightId}>
<Container className={css.popularShow} style={orderStyle} spotlightId={spotlightId} data-wheel-point={true}>
<SectionTitle
className={css.subTitle}
title={$L("POPULAR SHOW")}

View File

@@ -28,7 +28,7 @@ import css from "../../HomePanel/SubCategory/SubCategory.module.less";
const SpottableComponent = Spottable("div");
const Container = SpotlightContainerDecorator(
{
enterTo: "last-focused",
enterTo: null,
},
"div"
);
@@ -231,7 +231,7 @@ const SubCategory = ({
}
}, [handleItemFocus]);
return (
<Container style={orderStyle} spotlightId={spotlightId}>
<Container style={orderStyle} spotlightId={spotlightId} data-wheel-point={true}>
<ContainerBasic>
<CategoryNav
categoryInfos={categoryInfos}

View File

@@ -20,7 +20,7 @@ export default memo(function OnSaleContents({
selectedLgCatCd,
}) {
return (
<Container className={css.container} spotlightId={spotlightId}>
<Container className={css.container} spotlightId={spotlightId} data-wheel-point={true}>
<SectionTitle
data-title-index={contentsIndex}
title={saleNm}

View File

@@ -61,7 +61,7 @@ export default function OnSalePanel({ panelInfo }) {
const previousPanelIsDetail = Object.keys(panelInfo).length > 4;
const cbChangePageRef = useRef(null);
const pageIndexRef = useRef(0);
const focusedContainerIdRef = useRef(0);
useEffect(() => {
sendLogGNB(LOG_MENU.ON_SALE);
@@ -197,7 +197,7 @@ export default function OnSalePanel({ panelInfo }) {
isReadyForInitialFocusTarget,
panelInfo,
previousPanelIsDetail,
previousPanelIsHome,
previousPanelIsHome
]);
useEffect(() => {
@@ -277,7 +277,7 @@ export default function OnSalePanel({ panelInfo }) {
exprOrd,
lgCatCd,
targetId,
pageIndex: pageIndexRef.current,
focusedContainerId: focusedContainerIdRef.current,
},
})
);
@@ -287,32 +287,21 @@ export default function OnSalePanel({ panelInfo }) {
const handleTopButtonClick = useCallback(() => {
if(cbChangePageRef.current){
if (!firstFocusableTarget) {
cbChangePageRef.current(0, true);
}
if (!firstFocusableTarget) {
return;
}
setIsTopButtonClicked(true);
setTimeout(() => {
Spotlight.focus(firstFocusableTarget);
setIsTopButtonClicked(false);
});
}
}, [firstFocusableTarget]);
const pageSpotIds = useMemo(() => {
const spots = [];
if (saleInfos) {
for (let i = 0; i < saleInfos.length; i++) {
spots.push("sale-info-" + i);
}
}
return spots;
}, [saleInfos]);
const onPageChanged = useCallback((index) => {
pageIndexRef.current = index;
const onFocusedContainerId = useCallback((containerId) => {
focusedContainerIdRef.current = containerId;
}, []);
const onSaleNavClicked = useCallback((ev) => {
@@ -339,17 +328,16 @@ export default function OnSalePanel({ panelInfo }) {
/>
<TBody className={css.tBody} scrollable={false}>
{saleInfos && (
{saleInfos &&
<TVerticalPagenator
className={css.tVerticalPagenator}
spotlightId={"onSale_verticalPagenator"}
spotlightId={'onSale_verticalPagenator'}
enableFocusAction={false}
defaultPageIndex={panelInfo?.pageIndex ?? 0}
pageSpotIds={pageSpotIds}
onPageChanged={onPageChanged}
defaultContainerId={panelInfo?.focusedContainerId}
onFocusedContainerId={onFocusedContainerId}
cbChangePageRef={cbChangePageRef}
topMargin={36}
>
topMargin={36}>
<>
{saleInfos.map(
({ saleNm, saleProductInfos }, contentsIndex) => (
<OnSaleContents
@@ -371,8 +359,9 @@ export default function OnSalePanel({ panelInfo }) {
ariaLabel="Move to Top"
/>
)}
</>
</TVerticalPagenator>
)}
}
</TBody>
</>
)}