[TScroller, TVirtualGridList] 마우스 임계치 도달 시 오토 스크롤 적용, AutoScrollArea 추가

This commit is contained in:
hyunwoo93.cha
2024-02-29 00:24:09 +09:00
parent 2f1e617221
commit c7242a0270
8 changed files with 463 additions and 95 deletions

View File

@@ -0,0 +1,28 @@
import React, { useCallback } from "react";
import classNames from "classnames";
import Spottable from "@enact/spotlight/Spottable";
import css from "./AutoScrollArea.module.less";
const AutoScrollComponent = Spottable("div");
const POSITION = {
left: "left",
right: "right",
top: "top",
bottom: "bottom",
};
export default function AutoScrollArea({ position, onFocus, onBlur }) {
return (
<AutoScrollComponent
className={classNames(css.autoScrollArea, position && css[position])}
onFocus={onFocus}
onBlur={onBlur}
/>
);
}
export { POSITION };

View File

@@ -0,0 +1,36 @@
.autoScrollArea {
position: absolute;
z-index: 20;
// background-color: transparent; /* production */
background-color: #ff00001f; /* develop */
&.left {
width: 50px;
height: 100%;
top: 50%;
left: 0;
transform: translateY(-50%);
}
&.right {
width: 50px;
height: 100%;
top: 50%;
right: 0;
transform: translateY(-50%);
}
&.top {
width: 100%;
height: 50px;
top: 0;
left: 0;
}
&.bottom {
width: 100%;
height: 50px;
bottom: 0;
left: 0;
}
}

View File

@@ -1,11 +1,14 @@
import React, { useCallback, useEffect, 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 { AUTO_SCROLL_GAP } from "../../utils/Config";
import AutoScrollArea, { POSITION } from "../AutoScrollArea/AutoScrollArea";
import css from "./TScroller.module.less";
export default function TScroller({
@@ -19,11 +22,20 @@ export default function TScroller({
onScrollStart,
onScrollStop,
noScrollByWheel = false,
cbScrollTo,
autoScroll = direction === "horizontal",
...rest
}) {
const { cursorVisible } = useSelector((state) => state.common.appStatus);
const isScrolling = useRef(false);
const scrollPosition = useRef("top");
const scrollToRef = useRef(null);
const requestIdRef = useRef();
const scrollHorizontalPos = useRef(0);
const scrollVerticalPos = useRef(0);
const [onMoveScrollBarEffect, setOnMoveScrollBarEffect] = useState(false);
const [isMounted, setIsMounted] = useState(false);
@@ -36,14 +48,15 @@ export default function TScroller({
}, []);
const handleWheel = useCallback(() => {
if (!onMoveScrollBarEffect) {
if (!onMoveScrollBarEffect && isMounted) {
setOnMoveScrollBarEffect(true);
offMoveScrollBarEffect.start(() => setOnMoveScrollBarEffect(false));
} else {
return;
offMoveScrollBarEffect.start(() => {
if (isMounted) {
setOnMoveScrollBarEffect(false);
}
});
}
}, [offMoveScrollBarEffect, onMoveScrollBarEffect]);
}, [offMoveScrollBarEffect, onMoveScrollBarEffect, isMounted]);
useEffect(() => {
on("wheel", handleWheel, document.getElementById("TScroller"));
@@ -77,42 +90,152 @@ export default function TScroller({
scrollPosition.current = "top";
} else if (e.reachedEdgeInfo.bottom) {
scrollPosition.current = "bottom";
} else if (e.reachedEdgeInfo.left) {
scrollPosition.current = "left";
} else if (e.reachedEdgeInfo.right) {
scrollPosition.current = "right";
} else {
scrollPosition.current = "middle";
}
} else {
scrollPosition.current = "middle";
}
scrollHorizontalPos.current = e.scrollLeft;
scrollVerticalPos.current = e.scrollTop;
},
[onScrollStop]
);
const _cbScrollTo = useCallback(
(ref) => {
if (cbScrollTo) {
cbScrollTo(ref);
}
scrollToRef.current = ref;
},
[cbScrollTo]
);
const handleFocusAutoScroll = useCallback((position) => {
if (!autoScroll) return;
const scrollStep =
position === POSITION.right || position === POSITION.bottom
? AUTO_SCROLL_GAP
: -AUTO_SCROLL_GAP;
let start = null;
const animateScroll = (timestamp) => {
if (!start) start = timestamp;
const progress = timestamp - start;
const step = Math.min(progress / 1000, 1);
if (direction === "horizontal") {
scrollHorizontalPos.current += scrollStep * step;
scrollToRef.current({
position: { x: scrollHorizontalPos.current },
animate: false,
});
} else if (direction === "vertical") {
scrollVerticalPos.current += scrollStep * step;
scrollToRef.current({
position: { y: scrollVerticalPos.current },
animate: false,
});
}
if (
direction === "horizontal" &&
((position === POSITION.right && scrollPosition.current === "right") ||
(position === POSITION.left && scrollPosition.current === "left"))
) {
window.cancelAnimationFrame(requestIdRef.current);
} else if (
direction === "vertical" &&
((position === POSITION.bottom &&
scrollPosition.current === "bottom") ||
(position === POSITION.top && scrollPosition.current === "top"))
) {
window.cancelAnimationFrame(requestIdRef.current);
} else {
requestIdRef.current = window.requestAnimationFrame(animateScroll);
}
};
if (typeof window === "object") {
requestIdRef.current = window.requestAnimationFrame(animateScroll);
}
}, []);
const handleBlur = useCallback(() => {
if (requestIdRef.current && typeof window === "object") {
window.cancelAnimationFrame(requestIdRef.current);
}
}, [autoScroll, direction]);
return (
<Scroller
{...rest}
onScrollStart={_onScrollStart}
onScrollStop={_onScrollStop}
id="TScroller"
scrollMode={scrollMode || "translate"}
focusableScrollbar={focusableScrollbar}
<div
className={classNames(
className,
isMounted && css.tScroller,
onMoveScrollBarEffect ? css.onMove : ""
className ? className : null,
css.scrollerContainer
)}
direction={direction}
horizontalScrollbar={horizontalScrollbar}
verticalScrollbar={verticalScrollbar}
overscrollEffectOn={{
arrowKey: false,
drag: false,
pageKey: false,
track: false,
wheel: false,
}}
noScrollByWheel={noScrollByWheel}
>
{children}
</Scroller>
<Scroller
{...rest}
cbScrollTo={_cbScrollTo}
onScrollStart={_onScrollStart}
onScrollStop={_onScrollStop}
id="TScroller"
scrollMode={scrollMode || "translate"}
focusableScrollbar={focusableScrollbar}
className={classNames(
isMounted && css.tScroller,
onMoveScrollBarEffect ? css.onMove : ""
)}
direction={direction}
horizontalScrollbar={horizontalScrollbar}
verticalScrollbar={verticalScrollbar}
overscrollEffectOn={{
arrowKey: false,
drag: false,
pageKey: false,
track: false,
wheel: false,
}}
noScrollByWheel={noScrollByWheel}
>
{children}
</Scroller>
{direction === "horizontal" && cursorVisible && autoScroll && (
<AutoScrollArea
position={POSITION.left}
onFocus={() => handleFocusAutoScroll(POSITION.left)}
onBlur={handleBlur}
/>
)}
{direction === "horizontal" && cursorVisible && autoScroll && (
<AutoScrollArea
position={POSITION.right}
onFocus={() => handleFocusAutoScroll(POSITION.right)}
onBlur={handleBlur}
/>
)}
{direction === "vertical" && cursorVisible && autoScroll && (
<AutoScrollArea
position={POSITION.top}
onFocus={() => handleFocusAutoScroll(POSITION.top)}
onBlur={handleBlur}
/>
)}
{direction === "vertical" && cursorVisible && autoScroll && (
<AutoScrollArea
position={POSITION.bottom}
onFocus={() => handleFocusAutoScroll(POSITION.bottom)}
onBlur={handleBlur}
/>
)}
</div>
);
}

View File

@@ -7,10 +7,17 @@
@focusedBarColor: #transparent;
@verTrackColor: #f1efec;
.tScroller {
> div:nth-child(2) > div:nth-child(1) {
background-color: #fff;
width: 8px;
border-radius: 40px;
.scrollerContainer {
position: relative;
.tScroller {
width: 100%;
height: 100%;
> div:nth-child(2) > div:nth-child(1) {
background-color: #fff;
width: 8px;
border-radius: 40px;
}
}
}

View File

@@ -1,10 +1,13 @@
import React from "react";
import React, { useCallback, useRef } from "react";
import classNames from "classnames";
import { useSelector } from "react-redux";
import { VirtualGridList } from "@enact/sandstone/VirtualList";
import { AUTO_SCROLL_GAP } from "../../utils/Config";
import { scaleH, scaleW } from "../../utils/helperMethods";
import AutoScrollArea, { POSITION } from "../AutoScrollArea/AutoScrollArea";
import css from "./TVirtualGridList.module.less";
export default function TVirtualGridList({
@@ -22,26 +25,185 @@ export default function TVirtualGridList({
cbScrollTo,
spotlightId,
renderItem,
onScrollStart,
onScrollStop,
autoScroll = direction === "horizontal",
...rest
}) {
const { cursorVisible } = useSelector((state) => state.common.appStatus);
const isScrolling = useRef(false);
const scrollPosition = useRef("top");
const scrollToRef = useRef(null);
const requestIdRef = useRef();
const scrollHorizontalPos = useRef(0);
const scrollVerticalPos = useRef(0);
const _onScrollStart = useCallback(
(e) => {
if (onScrollStart) {
onScrollStart(e);
}
isScrolling.current = true;
},
[onScrollStart]
);
const _onScrollStop = useCallback(
(e) => {
if (onScrollStop) {
onScrollStop(e);
}
isScrolling.current = false;
if (e.reachedEdgeInfo) {
if (e.reachedEdgeInfo.top) {
scrollPosition.current = "top";
} else if (e.reachedEdgeInfo.bottom) {
scrollPosition.current = "bottom";
} else if (e.reachedEdgeInfo.left) {
scrollPosition.current = "left";
} else if (e.reachedEdgeInfo.right) {
scrollPosition.current = "right";
} else {
scrollPosition.current = "middle";
}
} else {
scrollPosition.current = "middle";
}
scrollHorizontalPos.current = e.scrollLeft;
scrollVerticalPos.current = e.scrollTop;
},
[onScrollStop]
);
const _cbScrollTo = useCallback(
(ref) => {
if (cbScrollTo) {
cbScrollTo(ref);
}
scrollToRef.current = ref;
},
[cbScrollTo]
);
const handleFocusAutoScroll = useCallback((position) => {
if (!autoScroll) return;
const scrollStep =
position === POSITION.right || position === POSITION.bottom
? AUTO_SCROLL_GAP
: -AUTO_SCROLL_GAP;
let start = null;
const animateScroll = (timestamp) => {
if (!start) start = timestamp;
const progress = timestamp - start;
const step = Math.min(progress / 1000, 1);
if (direction === "horizontal") {
scrollHorizontalPos.current += scrollStep * step;
scrollToRef.current({
position: { x: scrollHorizontalPos.current },
animate: false,
});
} else if (direction === "vertical") {
scrollVerticalPos.current += scrollStep * step;
scrollToRef.current({
position: { y: scrollVerticalPos.current },
animate: false,
});
}
if (
direction === "horizontal" &&
((position === POSITION.right && scrollPosition.current === "right") ||
(position === POSITION.left && scrollPosition.current === "left"))
) {
window.cancelAnimationFrame(requestIdRef.current);
} else if (
direction === "vertical" &&
((position === POSITION.bottom &&
scrollPosition.current === "bottom") ||
(position === POSITION.top && scrollPosition.current === "top"))
) {
window.cancelAnimationFrame(requestIdRef.current);
} else {
requestIdRef.current = window.requestAnimationFrame(animateScroll);
}
};
if (typeof window === "object") {
requestIdRef.current = window.requestAnimationFrame(animateScroll);
}
}, []);
const handleBlur = useCallback(() => {
if (requestIdRef.current && typeof window === "object") {
window.cancelAnimationFrame(requestIdRef.current);
}
}, [autoScroll, direction]);
return (
<VirtualGridList
className={classNames(css.tVirtualGridList, className ? className : null)}
direction={direction}
dataSize={dataSize}
itemRenderer={renderItem}
verticalScrollbar={verticalScrollbar}
horizontalScrollbar={horizontalScrollbar}
noScrollByWheel={noScrollByWheel}
scrollMode={scrollMode}
itemSize={{
minWidth: scaleW(itemWidth),
minHeight: scaleH(itemHeight),
}}
spacing={scaleW(spacing)}
cbScrollTo={cbScrollTo}
spotlightId={spotlightId}
{...rest}
/>
<div
className={classNames(
className ? className : null,
css.tVirtualGridListContainer
)}
>
<VirtualGridList
className={classNames(css.tVirtualGridList)}
onScrollStart={_onScrollStart}
onScrollStop={_onScrollStop}
direction={direction}
dataSize={dataSize}
itemRenderer={renderItem}
verticalScrollbar={verticalScrollbar}
horizontalScrollbar={horizontalScrollbar}
noScrollByWheel={noScrollByWheel}
scrollMode={scrollMode}
itemSize={{
minWidth: scaleW(itemWidth),
minHeight: scaleH(itemHeight),
}}
spacing={scaleW(spacing)}
cbScrollTo={_cbScrollTo}
spotlightId={spotlightId}
{...rest}
/>
{direction === "horizontal" && cursorVisible && autoScroll && (
<AutoScrollArea
position={POSITION.left}
onFocus={() => handleFocusAutoScroll(POSITION.left)}
onBlur={handleBlur}
/>
)}
{direction === "horizontal" && cursorVisible && autoScroll && (
<AutoScrollArea
position={POSITION.right}
onFocus={() => handleFocusAutoScroll(POSITION.right)}
onBlur={handleBlur}
/>
)}
{direction === "vertical" && cursorVisible && autoScroll && (
<AutoScrollArea
position={POSITION.top}
onFocus={() => handleFocusAutoScroll(POSITION.top)}
onBlur={handleBlur}
/>
)}
{direction === "vertical" && cursorVisible && autoScroll && (
<AutoScrollArea
position={POSITION.bottom}
onFocus={() => handleFocusAutoScroll(POSITION.bottom)}
onBlur={handleBlur}
/>
)}
</div>
);
}

View File

@@ -1,7 +1,11 @@
.tVirtualGridList {
overflow: unest;
.tVirtualGridListContainer {
position: relative;
> div {
overflow: unset !important;
.tVirtualGridList {
overflow: unset;
> div {
overflow: unset !important;
}
}
}

View File

@@ -18,35 +18,39 @@ export const getConnectionStatus = ({ onSuccess, onFailure, onComplete }) => {
};
export const createToast = (message) => {
if (typeof window === 'object' && !window.PalmSystem) {
if (typeof window === "object" && !window.PalmSystem) {
console.log("LUNA SEND createToast message", message);
return;
}
return new LS2Request().send({
service: 'luna://com.webos.notification',
method: 'createToast',
parameters: {
message: message,
iconUrl: '',
noaction: true
},
onSuccess: (res) => {
console.log("LUNA SEND createToast success", message);
},
onFailure: (err) => {
console.log("LUNA SEND createToast failed", err);
}
});
return;
}
return new LS2Request().send({
service: "luna://com.webos.notification",
method: "createToast",
parameters: {
message: message,
iconUrl: "",
noaction: true,
},
onSuccess: (res) => {
console.log("LUNA SEND createToast success", message);
},
onFailure: (err) => {
console.log("LUNA SEND createToast failed", err);
},
});
};
let httpHeaderHandler = null;
export const getHttpHeaderForServiceRequest = ({onSuccess, onFailure, onComplete}) => {
export const getHttpHeaderForServiceRequest = ({
onSuccess,
onFailure,
onComplete,
}) => {
if (typeof window === "object" && window.PalmSystem) {
if (process.env.REACT_APP_MODE === "DEBUG") {
console.log("LUNA SEND getHttpHeaderForServiceRequest");
return "Some Hard Coded Mock Data";
} else {
if(httpHeaderHandler){
if (httpHeaderHandler) {
httpHeaderHandler.cancel();
}
httpHeaderHandler = new LS2Request().send({
@@ -60,23 +64,26 @@ export const getHttpHeaderForServiceRequest = ({onSuccess, onFailure, onComplete
});
return httpHeaderHandler;
}
}else{
} else {
onSuccess({
"HOST": "US.nextlgsdp.com",
"X-User-Number": "US2401051532595",
"Authorization": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJuZXh0bGdzZHAuY29tIiwiYXVkIjoibmV4dGxnc2RwLmNvbSIsImlhdCI6MTcwNzc4NTUyNSwiZXhwIjoxNzA3NzkyNzI1LCJtYWNBZGRyZXNzIjoiZWVkMDQ2NjdiNjUzOWU3YmQxMDA1OTljYjBkYTI5ZjRjZTgyZGZlOGZkNzIzMDAxZGVmMjg4NWRkNWZiODRmNWNiMzZlM2QwNzYzNWZjZGJjYWNjNGVjMzI5NWIwNjZjOTMwNmNmNDI1ZGQzMmQ2MDMxMjc1NWNkOTIyNjEwMzcifQ.vqPdYGnN46diesDBLzA4UhACCJVdIycLs7wZu9M55Hc",
"X-Authentication": "MkOLvUocrJ69RH/iV1ZABJhjR2g=",
"X-Device-ID": "OemUY5qbPITZv96QKlxrtcqT6ypeX6us2qANLng3/0QCUhv2mecK1UDTMYb/hjpjey9dC/kFycc/5R8u+oK56JIWyYC4V278z64YDPKbDXIsd+eECvyf+Rdm8BneIUPM",
"X-Device-Product": "webOSTV 5.0",
"X-Device-Platform": "W20P",
"X-Device-Model": "HE_DTV_W20P_AFADATAA",
"X-Device-Eco-Info": "1",
"X-Device-Country": "US",
"X-Device-Language":"en-US",
"X-Device-Netcast-Platform-Version": "5.0.0",
"X-Device-Publish-Flag": "N",
"X-Device-Fck": "253",
"X-Device-Eula": "additionalDataAllowed,takeOnAllowed,networkAllowed,generalTermsAllowed,chpAllowed,customAdAllowed,acrOnAllowed,voice2Allowed,voiceAllowed,acrAdAllowed"
});
HOST: "US.nextlgsdp.com",
"X-User-Number": "US2401051532595",
Authorization:
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJuZXh0bGdzZHAuY29tIiwiYXVkIjoibmV4dGxnc2RwLmNvbSIsImlhdCI6MTcwNzc4NTUyNSwiZXhwIjoxNzA3NzkyNzI1LCJtYWNBZGRyZXNzIjoiZWVkMDQ2NjdiNjUzOWU3YmQxMDA1OTljYjBkYTI5ZjRjZTgyZGZlOGZkNzIzMDAxZGVmMjg4NWRkNWZiODRmNWNiMzZlM2QwNzYzNWZjZGJjYWNjNGVjMzI5NWIwNjZjOTMwNmNmNDI1ZGQzMmQ2MDMxMjc1NWNkOTIyNjEwMzcifQ.vqPdYGnN46diesDBLzA4UhACCJVdIycLs7wZu9M55Hc",
"X-Authentication": "MkOLvUocrJ69RH/iV1ZABJhjR2g=",
"X-Device-ID":
"OemUY5qbPITZv96QKlxrtcqT6ypeX6us2qANLng3/0QCUhv2mecK1UDTMYb/hjpjey9dC/kFycc/5R8u+oK56JIWyYC4V278z64YDPKbDXIsd+eECvyf+Rdm8BneIUPM",
"X-Device-Product": "webOSTV 5.0",
"X-Device-Platform": "W20P",
"X-Device-Model": "HE_DTV_W20P_AFADATAA",
"X-Device-Eco-Info": "1",
"X-Device-Country": "US",
"X-Device-Language": "en-US",
"X-Device-Netcast-Platform-Version": "5.0.0",
"X-Device-Publish-Flag": "N",
"X-Device-Fck": "253",
"X-Device-Eula":
"additionalDataAllowed,takeOnAllowed,networkAllowed,generalTermsAllowed,chpAllowed,customAdAllowed,acrOnAllowed,voice2Allowed,voiceAllowed,acrAdAllowed",
});
}
};
};

View File

@@ -48,3 +48,4 @@ export const ACTIVE_POPUP = {
};
export const AUTO_SCROLL_DELAY = 600;
export const AUTO_SCROLL_GAP = 20;