[OnSalePanel] Home에서 진입시 initial focus target 변경

Detail Notes :
This commit is contained in:
younghoon100.park
2024-04-15 19:55:11 +09:00
parent 4fb37e2a23
commit 13ac88ef29
8 changed files with 297 additions and 174 deletions

View File

@@ -1,29 +1,25 @@
import React, { useCallback, useEffect, useRef } from "react";
import React, { memo, useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useSelector } from "react-redux";
import { Job } from "@enact/core/util";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import {
getContainerId,
setContainerLastFocusedElement,
} from "@enact/spotlight/src/container";
import { pushPanel, updatePanel } from "../../../../actions/panelActions";
import TItemCard from "../../../../components/TItemCard/TItemCard";
import TScroller from "../../../../components/TScroller/TScroller";
import useScrollReset from "../../../../hooks/useScrollReset";
import useScrollTo from "../../../../hooks/useScrollTo";
import useScrollTopByDistance from "../../../../hooks/useScrollTopByDistance";
import { panel_names } from "../../../../utils/Config";
import { SpotlightIds } from "../../../../utils/SpotlightIds";
import css from "./OnSaleProductList.module.less";
import OnSaleProductListItem from "./OnSaleProductListItem/OnSaleProductListItem";
const Container = SpotlightContainerDecorator(
{ leaveFor: { right: "" }, enterTo: "last-focused" },
"div"
);
export default function OnSaleProductList({
export default memo(function OnSaleProductList({
index,
isTopButtonClicked,
saleProductInfos,
@@ -31,33 +27,30 @@ export default function OnSaleProductList({
selectedLgCatCd,
}) {
const { getScrollTo, scrollLeft } = useScrollTo();
const { handleScrollReset, handleStopScrolling } = useScrollReset(
scrollLeft,
true
);
const { scrollTopByDistance } = useScrollTopByDistance();
const dispatch = useDispatch();
const panelInfo = useSelector((state) => state.panels.panels[0]?.panelInfo);
const { cursorVisible } = useSelector((state) => state.common.appStatus);
const scrollLeftTimeoutRef = useRef();
const scrollLeftJob = useRef(new Job((func) => func(), 0));
useEffect(() => {
const scrollLeftJobValue = scrollLeftJob.current;
if (!panelInfo?.exprOrd) {
return;
}
if (panelInfo?.exprOrd === index + 1) {
const { x } = panelInfo;
scrollLeftTimeoutRef.current = setTimeout(() => scrollLeft({ x }));
scrollLeftJobValue.start(() => scrollLeft({ x }));
}
return () => {
if (panelInfo?.exprOrd === index + 1) {
clearTimeout(scrollLeftTimeoutRef.current);
scrollLeftJobValue.stop();
}
};
}, []);
}, [index, panelInfo, scrollLeft]);
useEffect(() => {
const containerId = "on-sale-product-list-id-" + index;
@@ -65,77 +58,20 @@ export default function OnSaleProductList({
if (container) {
const childrenId = getContainerId(container?.children[0].children[0]);
childrenId &&
if (childrenId) {
setContainerLastFocusedElement(null, [containerId, childrenId]);
}
}
scrollLeft();
scrollTopBody();
}, [selectedLgCatCd]);
}, [index, scrollLeft, scrollTopBody, selectedLgCatCd]);
useEffect(() => {
if (index === 0 && isTopButtonClicked) {
scrollLeft();
}
}, [isTopButtonClicked]);
const handleBlur = useCallback((itemIndex) => {
if (itemIndex === 0) {
handleStopScrolling();
}
}, []);
const handleClick = useCallback(
(e, patnrId, prdtId) => {
const tItemCard = e.currentTarget;
const tBody = document.querySelector(
`[data-spotlight-id="${SpotlightIds.TBODY}"]`
);
const xContainer = tItemCard?.parentNode?.parentNode?.parentNode;
const yContainer = tBody?.children[0]?.children[0]?.children[0];
if (xContainer && yContainer) {
const x = xContainer.scrollLeft;
const y = yContainer.scrollTop;
dispatch(
updatePanel({
name: panel_names.ON_SALE_PANEL,
panelInfo: { ...panelInfo, x, y },
})
);
}
dispatch(
pushPanel({
name: panel_names.DETAIL_PANEL,
panelInfo: { patnrId, prdtId },
})
);
},
[dispatch]
);
const handleFocus = useCallback(
(itemIndex) => {
if (itemIndex === 0) {
handleScrollReset();
}
if (cursorVisible) {
return;
}
scrollTopByDistance(
`[data-marker="scroll-marker"]`,
`[data-title-index="${index}"]`,
scrollTopBody,
36
);
},
[cursorVisible]
);
}, [index, isTopButtonClicked, scrollLeft]);
return (
<Container
@@ -149,38 +85,18 @@ export default function OnSaleProductList({
noScrollByWheel
>
<ul>
{saleProductInfos.map(
(
{
imgUrl,
lgCatCd,
patnrId,
prdtNm,
prdtId,
priceInfo,
offerInfo,
},
itemIndex
) => (
<li key={"sale-product-info-" + prdtId}>
<TItemCard
data-lg-category-code={lgCatCd}
data-exposure-order={index + 1}
imageAlt={prdtNm}
imageSource={imgUrl}
onBlur={() => handleBlur(itemIndex)}
onClick={(e) => handleClick(e, patnrId, prdtId)}
onFocus={() => handleFocus(itemIndex)}
priceInfo={priceInfo}
productId={prdtId}
productName={prdtNm}
offerInfo={offerInfo}
/>
</li>
)
)}
{saleProductInfos.map((saleProductInfosItem, itemIndex) => (
<OnSaleProductListItem
index={index}
itemIndex={itemIndex}
key={"sale-product-infos-" + itemIndex}
saleProductInfosItem={saleProductInfosItem}
scrollLeft={scrollLeft}
scrollTopBody={scrollTopBody}
/>
))}
</ul>
</TScroller>
</Container>
);
}
});

View File

@@ -13,15 +13,6 @@
display: flex;
align-items: center;
padding: 22px 0 22px 60px;
> li {
flex: none;
// tItemCard
> div {
margin-right: 18px;
}
}
}
}
}

View File

@@ -0,0 +1,114 @@
import React, { memo, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { pushPanel, updatePanel } from "../../../../../actions/panelActions";
import TItemCard from "../../../../../components/TItemCard/TItemCard";
import useScrollReset from "../../../../../hooks/useScrollReset";
import useScrollTopByDistance from "../../../../../hooks/useScrollTopByDistance";
import { panel_names } from "../../../../../utils/Config";
import { SpotlightIds } from "../../../../../utils/SpotlightIds";
import css from "./OnSaleProductListItem.module.less";
export default memo(function OnSaleProductListItem({
index,
itemIndex,
saleProductInfosItem,
scrollLeft,
scrollTopBody,
}) {
const { handleScrollReset, handleStopScrolling } = useScrollReset(
scrollLeft,
true
);
const { scrollTopByDistance } = useScrollTopByDistance();
const dispatch = useDispatch();
const panelInfo = useSelector((state) => state.panels.panels[0]?.panelInfo);
const { cursorVisible } = useSelector((state) => state.common.appStatus);
const { imgUrl, lgCatCd, patnrId, prdtNm, prdtId, priceInfo, offerInfo } =
saleProductInfosItem;
const handleBlur = useCallback(() => {
if (itemIndex === 0) {
handleStopScrolling();
}
}, [itemIndex, handleStopScrolling]);
const handleClick = useCallback(
(e) => {
const tItemCard = e.currentTarget;
const tBody = document.querySelector(
`[data-spotlight-id="${SpotlightIds.TBODY}"]`
);
const xContainer = tItemCard?.parentNode?.parentNode?.parentNode;
const yContainer = tBody?.children[0]?.children[0]?.children[0];
if (xContainer && yContainer) {
const x = xContainer.scrollLeft;
const y = yContainer.scrollTop;
dispatch(
updatePanel({
name: panel_names.ON_SALE_PANEL,
panelInfo: { ...panelInfo, x, y },
})
);
}
dispatch(
pushPanel({
name: panel_names.DETAIL_PANEL,
panelInfo: { patnrId, prdtId },
})
);
},
[dispatch, panelInfo, patnrId, prdtId]
);
const handleFocus = useCallback(() => {
if (itemIndex === 0) {
handleScrollReset();
}
if (cursorVisible) {
return;
}
scrollTopByDistance(
`[data-marker="scroll-marker"]`,
`[data-title-index="${index}"]`,
scrollTopBody,
36
);
}, [
cursorVisible,
handleScrollReset,
index,
itemIndex,
scrollTopBody,
scrollTopByDistance,
]);
return (
<li className={css.item}>
<TItemCard
data-lg-category-code={lgCatCd}
data-exposure-order={index + 1}
imageAlt={prdtNm}
imageSource={imgUrl}
onBlur={handleBlur}
onClick={handleClick}
onFocus={handleFocus}
priceInfo={priceInfo}
productId={prdtId}
productName={prdtNm}
offerInfo={offerInfo}
/>
</li>
);
});

View File

@@ -0,0 +1,8 @@
.item {
flex: none;
// tItemCard
> div {
margin-right: 18px;
}
}

View File

@@ -0,0 +1,6 @@
{
"main": "OnSaleProductListItem.jsx",
"styles": [
"OnSaleProductListItem.module.less"
]
}

View File

@@ -58,7 +58,7 @@ export default function OnSaleNav({
scrollLeft({ x });
}
}
}, []);
}, [panelInfo?.lgCatCd, scrollLeft]);
return (
<Container className={css.nav}>
@@ -68,12 +68,11 @@ export default function OnSaleNav({
noScrollByWheel
>
<ul>
{categoryInfos.map((categoryInfo, index) => (
{categoryInfos.map((categoryInfosItem, itemIndex) => (
<OnSaleNavItem
categoryInfo={categoryInfo}
index={index}
key={"category-info" + index}
panelInfo={panelInfo}
categoryInfosItem={categoryInfosItem}
key={"category-info" + itemIndex}
itemIndex={itemIndex}
scrollTopBody={scrollTopBody}
selectedLgCatCd={selectedLgCatCd}
setFirstFocusableTarget={setFirstFocusableTarget}

View File

@@ -14,8 +14,8 @@ import css from "./OnSaleNavItem.module.less";
const SpottableComponent = Spottable("li");
export default memo(function OnSaleNavItem({
categoryInfo,
index,
categoryInfosItem,
itemIndex,
panelInfo,
scrollTopBody,
selectedLgCatCd,
@@ -25,27 +25,33 @@ export default memo(function OnSaleNavItem({
}) {
const dispatch = useDispatch();
const { lgCatCd, lgCatNm } = categoryInfo;
const { lgCatCd, lgCatNm } = categoryInfosItem;
const handleClick = useCallback(
(lgCatCd) => {
if (selectedLgCatCd === lgCatCd) {
return scrollTopBody();
}
const handleClick = useCallback(() => {
if (selectedLgCatCd === lgCatCd) {
return scrollTopBody();
}
dispatch(
updatePanel({
name: panel_names.ON_SALE_PANEL,
})
);
dispatch(
updatePanel({
name: panel_names.ON_SALE_PANEL,
panelInfo: { ...panelInfo, exprOrd: null },
})
);
setContainerLastFocusedElement(null, [SpotlightIds.TBODY]);
setSelectedLgCatCd(lgCatCd);
setFirstFocusableTarget();
scrollTopBody();
},
[dispatch, selectedLgCatCd]
);
setContainerLastFocusedElement(null, [SpotlightIds.TBODY]);
setSelectedLgCatCd(lgCatCd);
setFirstFocusableTarget();
scrollTopBody();
}, [
dispatch,
lgCatCd,
panelInfo,
scrollTopBody,
selectedLgCatCd,
setFirstFocusableTarget,
setSelectedLgCatCd,
]);
return (
<SpottableComponent
@@ -55,9 +61,9 @@ export default memo(function OnSaleNavItem({
? selectedLgCatCd === lgCatCd && css.selected
: panelInfo?.lgCatCd
? panelInfo.lgCatCd === lgCatCd && css.selected
: index === 0 && css.selected
: itemIndex === 0 && css.selected
)}
onClick={() => handleClick(lgCatCd)}
onClick={handleClick}
id={"spotlightId-" + lgCatCd}
spotlightId={"spotlightId-" + lgCatCd}
{...rest}

View File

@@ -40,19 +40,37 @@ export default function OnSalePanel() {
const [categories, setCategories] = useState();
const [firstFocusableTarget, setFirstFocusableTarget] = useState();
const [isInitialFocusOccurred, setIsInitialFocusOccurred] = useState(false);
const [isInitialRendered, setIsInitialRendered] = useState(true);
const [isReadyForInitialFocusTarget, setIsReadyForInitialFocusTarget] =
useState(false);
const [isTopButtonClicked, setIsTopButtonClicked] = useState(false);
const [selectedLgCatCd, setSelectedLgCatCd] = useState();
const [spotlightDisabled, setSpotlightDisabled] = useState(true);
const timeoutRef = useRef();
const firstFocusableItemTimeoutRef = useRef();
const initialFocusTimeoutRef = useRef();
const hasPanelInfo = Object.keys(panelInfo).length > 0;
const previousPanelIsDetail = Object.keys(panelInfo).length === 5;
const previousPanelIsHome = Object.keys(panelInfo).length === 1;
const previousPanelIsDetail = Object.keys(panelInfo).length >= 5;
const previousPanelIsHome = Object.keys(panelInfo).length === 2;
const isLoading = onSaleDataStatus !== "fulfilled";
useEffect(() => {
if (panelInfo) {
console.log(
"%c pyh panelInfo",
"background:pink; color:black",
panelInfo
);
console.log(
"%c pyh panelInfo length",
"background:pink; color:black",
Object.keys(panelInfo)?.length
);
}
}, [panelInfo]);
useEffect(() => {
const showLoadingPanel = isLoading
? { show: true, type: "wait" }
@@ -64,6 +82,10 @@ export default function OnSalePanel() {
useEffect(() => {
if (isInitialRendered) {
if (previousPanelIsDetail) {
console.log(
"%c pyh 1 initial Detail effect",
"background: blue; color: white"
);
return setIsInitialRendered(false);
}
@@ -73,26 +95,50 @@ export default function OnSalePanel() {
props = { ...props, lgCatCd: panelInfo.lgCatCd };
}
console.log(
"%c pyh 1 initial Home or GNB effect",
"background: blue; color: white"
);
dispatch(getOnSaleInfo(props));
dispatch(copyCategoryInfos(categoryInfos));
setIsInitialRendered(false);
}
}, [dispatch, isInitialRendered]);
}, [
categoryInfos,
dispatch,
isInitialRendered,
panelInfo?.lgCatCd,
previousPanelIsDetail,
previousPanelIsHome,
]);
useEffect(() => {
if (!categories) {
if (previousPanelIsDetail) {
console.log(
"%c pyh 2 copied category effect",
"background: blue; color: white"
);
return setCategories(copiedCategoryInfos);
}
if (categoryInfos) {
console.log(
"%c pyh 2 category effect",
"background: blue; color: white"
);
setCategories(categoryInfos);
}
}
}, [categories, categoryInfos, copiedCategoryInfos]);
}, [categories, categoryInfos, copiedCategoryInfos, previousPanelIsDetail]);
useEffect(() => {
if (selectedLgCatCd) {
console.log(
"%c pyh change LgCatCd effect",
"background: red; color: white"
);
dispatch(
getOnSaleInfo({ lgCatCd: selectedLgCatCd, saleInfosIncFlag: "Y" })
);
@@ -101,37 +147,69 @@ export default function OnSalePanel() {
useEffect(() => {
if (categories && saleInfos) {
if (!firstFocusableTarget) {
const prdtId = saleInfos[0].saleProductInfos[0].prdtId;
console.log(
"%c pyh saleInfos effect, first focusable target is ready",
"background: red; color: white"
);
setFirstFocusableTarget("spotlightId-" + removeDotAndColon(prdtId));
setIsReadyForInitialFocusTarget(true);
}
const prdtId = saleInfos[0]?.saleProductInfos[0]?.prdtId;
console.log("pyh prdtId", prdtId);
setFirstFocusableTarget("spotlightId-" + removeDotAndColon(prdtId));
setIsReadyForInitialFocusTarget(true);
}
}, [categories, saleInfos]);
useEffect(() => {
if (isReadyForInitialFocusTarget) {
if (isReadyForInitialFocusTarget && !isInitialFocusOccurred) {
let targetId;
if (hasPanelInfo) {
targetId = panelInfo?.targetId ?? "spotlightId-" + panelInfo?.lgCatCd;
if (previousPanelIsDetail) {
targetId = panelInfo?.targetId;
}
if (previousPanelIsHome) {
targetId = "spotlightId-" + panelInfo?.prdtId;
}
if (panelInfo?.y) {
const { y } = panelInfo;
scrollTopBody({ y, animate: false });
}
} else {
targetId = "spotlightId-" + categoryInfos[0]?.lgCatCd;
if (categories) {
targetId = "spotlightId-" + categories[0]?.lgCatCd;
}
}
const initialFocusTarget = document.querySelector(
`[data-spotlight-id="${targetId}"]`
);
setSpotlightDisabled(false);
initialFocusTarget && Spotlight.focus(initialFocusTarget);
initialFocusTimeoutRef.current = setTimeout(() => {
const initialFocusTarget = document.querySelector(
`[data-spotlight-id="${targetId}"]`
);
console.log(
"%c pyh *** initial focus *** effect",
"background: green; color: white"
);
if (initialFocusTarget) {
Spotlight.focus(initialFocusTarget);
setIsInitialFocusOccurred(true);
}
});
}
}, [isReadyForInitialFocusTarget]);
}, [
categories,
hasPanelInfo,
isInitialFocusOccurred,
isReadyForInitialFocusTarget,
panelInfo,
previousPanelIsDetail,
previousPanelIsHome,
scrollTopBody,
]);
useEffect(() => {
return () => {
@@ -152,9 +230,10 @@ export default function OnSalePanel() {
);
}
clearTimeout(timeoutRef.current);
clearTimeout(initialFocusTimeoutRef.current);
clearTimeout(firstFocusableItemTimeoutRef.current);
};
}, []);
}, [dispatch]);
const handleTopButtonClick = useCallback(() => {
if (!firstFocusableTarget) {
@@ -164,14 +243,18 @@ export default function OnSalePanel() {
scrollTopBody();
setIsTopButtonClicked(true);
timeoutRef.current = setTimeout(() => {
firstFocusableItemTimeoutRef.current = setTimeout(() => {
Spotlight.focus(firstFocusableTarget);
setIsTopButtonClicked(false);
});
}, [firstFocusableTarget]);
}, [firstFocusableTarget, scrollTopBody]);
return (
<TPanel className={css.tPanel} id={"tPanel"}>
<TPanel
className={css.tPanel}
id={"tPanel"}
spotlightDisabled={spotlightDisabled}
>
{categories && (
<React.Fragment>
<OnSaleNav