[OnSalePanel] issue fix & scroll 설정

Detail Notes :
1. issue fix, SHOPTIME-2079
2. issue fix, SHOPTIME-2081
3. scroll 설정, 신규 scenario
This commit is contained in:
younghoon100.park
2024-04-05 18:07:13 +09:00
parent 75f71418dc
commit e45219ee81
8 changed files with 316 additions and 168 deletions

View File

@@ -10,7 +10,7 @@ export const getOnSaleInfo = (props) => (dispatch, getState) => {
dispatch({
type: types.GET_ON_SALE_INFO,
payload: {
data: getState().onSale.onSaleData.data,
...getState().onSale.onSaleData,
status: "pending",
},
});

View File

@@ -16,17 +16,18 @@ export default memo(function OnSaleContents({
isTopButtonClicked,
saleNm,
saleProductInfos,
scrollTop,
scrollTopBody,
selectedLgCatCd,
}) {
return (
<Container className={css.container}>
<SectionTitle title={saleNm} />
<SectionTitle data-title={saleNm} title={saleNm} />
<OnSaleProductList
index={index}
isTopButtonClicked={isTopButtonClicked}
saleNm={saleNm}
saleProductInfos={saleProductInfos}
scrollTop={scrollTop}
scrollTopBody={scrollTopBody}
selectedLgCatCd={selectedLgCatCd}
/>
</Container>

View File

@@ -2,9 +2,8 @@
@import "../../../style/utils.module.less";
.container {
// sectionTitle
h2 {
margin: 58px 0 24px;
> h2 {
margin: 36px 0 2px;
padding-left: 60px;
}
}

View File

@@ -1,42 +1,79 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback, useEffect, useRef } from "react";
import { useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import {
getContainerId,
setContainerLastFocusedElement,
} from "@enact/spotlight/src/container";
import { pushPanel } from "../../../../actions/panelActions";
import { pushPanel, updatePanel } from "../../../../actions/panelActions";
import TItemCard from "../../../../components/TItemCard/TItemCard";
import TVirtualGridList from "../../../../components/TVirtualGridList/TVirtualGridList";
import TScroller from "../../../../components/TScroller/TScroller";
import useScrollReset from "../../../../hooks/useScrollReset";
import useScrollTo from "../../../../hooks/useScrollTo";
import { panel_names } from "../../../../utils/Config";
import { SpotlightIds } from "../../../../utils/SpotlightIds";
import css from "./OnSaleProductList.module.less";
const Container = SpotlightContainerDecorator(
{ leaveFor: { right: "" }, enterTo: "last-focused" },
"div"
);
export default function OnSaleProductList({
index,
isTopButtonClicked,
saleNm,
saleProductInfos,
scrollTop,
scrollTopBody,
selectedLgCatCd,
}) {
const { getScrollTo, scrollLeft } = useScrollTo();
const { handleScrollReset, handleStopScrolling } = useScrollReset(
scrollLeft,
true
);
const dispatch = useDispatch();
const [isFirstGridItemFocused, setIsFirstGridItemFocused] = useState(false);
const panelInfo = useSelector((state) => state.panels.panels[0]?.panelInfo);
const { cursorVisible } = useSelector((state) => state.common.appStatus);
const focusTimeoutRef = useRef();
const scrollLeftTimeoutRef = useRef();
const scrollTopBodyTimeoutRef = useRef();
useEffect(() => {
const containerId = "onSaleProductListId-" + index;
if (!cursorVisible) {
return;
}
clearTimeout(scrollLeftTimeoutRef.current);
clearTimeout(scrollTopBodyTimeoutRef.current);
if (panelInfo?.exprOrd === index + 1) {
const { x, y } = panelInfo;
scrollLeftTimeoutRef.current = setTimeout(() => scrollLeft({ x }));
scrollTopBodyTimeoutRef.current = setTimeout(() => scrollTopBody({ y }));
}
}, [cursorVisible]);
useEffect(() => {
const containerId = "on-sale-product-list-id-" + index;
const container = document.getElementById(containerId);
if (container) {
const childrenId = getContainerId(container?.children[0].children[0]);
childrenId && setContainerLastFocusedElement(null, [childrenId]);
childrenId &&
setContainerLastFocusedElement(null, [containerId, childrenId]);
}
scrollLeft();
scrollTopBody();
}, [selectedLgCatCd]);
useEffect(() => {
@@ -46,65 +83,128 @@ export default function OnSaleProductList({
}, [isTopButtonClicked]);
useEffect(() => {
if (isFirstGridItemFocused) {
scrollTop();
return () => {
clearTimeout(focusTimeoutRef.current);
clearTimeout(scrollLeftTimeoutRef.current);
clearTimeout(scrollTopBodyTimeoutRef.current);
};
}, []);
const scrollTopBodyByDistance = useCallback(() => {
const marker = document.querySelector(`[data-marker="scroll-marker"]`);
const sectionTitle = document.querySelector(`[data-title="${saleNm}"]`);
if (marker && sectionTitle) {
const markerRect = marker.getBoundingClientRect();
const sectionTitleRect = sectionTitle.getBoundingClientRect();
const distacne = sectionTitleRect.top - markerRect.top;
scrollTopBody({ y: distacne });
}
}, [isFirstGridItemFocused]);
const handleFocus = useCallback(() => {
index === 0 && setIsFirstGridItemFocused(true);
}, []);
const handleBlur = useCallback(() => {
index === 0 && setIsFirstGridItemFocused(false);
const handleBlur = useCallback((itemIndex) => {
if (itemIndex === 0) {
handleStopScrolling();
}
clearTimeout(focusTimeoutRef.current);
}, []);
const handleClick = useCallback((patnrId, prdtId) => {
dispatch(
pushPanel({
name: panel_names.DETAIL_PANEL,
panelInfo: { patnrId, prdtId },
})
);
}, []);
const handleClick = useCallback(
(e, patnrId, prdtId) => {
const tItemCard = e.currentTarget;
const tBody = document.querySelector(
`[data-spotlight-id="${SpotlightIds.TBODY}"]`
);
const renderItem = useCallback(
({ index, ...rest }) => {
const { imgUrl, patnrId, prdtNm, prdtId, priceInfo, offerInfo } =
saleProductInfos[index];
const xContainer = tItemCard?.parentNode?.parentNode?.parentNode;
const yContainer = tBody?.children[0]?.children[0]?.children[0];
return (
<TItemCard
imageAlt={prdtNm}
imageSource={imgUrl}
onBlur={handleBlur}
onClick={() => handleClick(patnrId, prdtId)}
onFocus={handleFocus}
priceInfo={priceInfo}
productId={prdtId}
productName={prdtNm}
offerInfo={offerInfo}
{...rest}
/>
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 },
})
);
},
[saleProductInfos]
[dispatch]
);
const handleFocus = useCallback(
(itemIndex) => {
if (itemIndex === 0) {
handleScrollReset();
}
if (cursorVisible) {
return;
}
let scrollTop = index === 0 ? scrollTopBody : scrollTopBodyByDistance;
focusTimeoutRef.current = setTimeout(scrollTop);
},
[cursorVisible]
);
return (
<div className={css.container} id={"onSaleProductListId-" + index}>
{saleProductInfos && (
<TVirtualGridList
cbScrollTo={getScrollTo}
className={css.tVirtualGridList}
dataSize={saleProductInfos.length}
direction="horizontal"
itemHeight={438}
itemWidth={324}
spacing={18}
renderItem={renderItem}
/>
)}
</div>
<Container
className={css.container}
id={"on-sale-product-list-id-" + index}
spotlightId={"on-sale-product-list-id-" + index}
>
<TScroller
cbScrollTo={getScrollTo}
direction="horizontal"
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>
)
)}
</ul>
</TScroller>
</Container>
);
}

View File

@@ -3,14 +3,23 @@
.container {
position: relative;
.size(@w: 100%, @h: 438px);
.size(@w: 100%, @h: 482px);
// tVirtualGridListContainer
// tScroller
> div:nth-child(1) {
.size(@w: inherit, @h: inherit);
.size(@w: 100%, @h: 482px);
&.tVirtualGridList {
padding-left: 60px;
ul {
display: flex;
align-items: center;
padding: 22px 0 22px 60px;
> li {
// tItemCard
> div {
margin-right: 18px;
}
}
}
}
}

View File

@@ -1,12 +1,11 @@
import React, { useEffect } from 'react';
import React, { useEffect } from "react";
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import TScroller from '../../../components/TScroller/TScroller';
import useScrollTo from '../../../hooks/useScrollTo';
import css from './OnSaleNav.module.less';
import OnSaleNavItem from './OnSaleNavItem/OnSaleNavItem';
import TScroller from "../../../components/TScroller/TScroller";
import useScrollTo from "../../../hooks/useScrollTo";
import css from "./OnSaleNav.module.less";
import OnSaleNavItem from "./OnSaleNavItem/OnSaleNavItem";
const Container = SpotlightContainerDecorator(
{ leaveFor: { right: "" }, enterTo: "last-focused" },
@@ -16,7 +15,7 @@ const Container = SpotlightContainerDecorator(
export default function OnSaleNav({
categoryInfos,
panelInfo,
scrollTop,
scrollTopBody,
selectedLgCatCd,
setSelectedLgCatCd,
}) {
@@ -74,7 +73,7 @@ export default function OnSaleNav({
index={index}
key={"categoryInfo" + index}
panelInfo={panelInfo}
scrollTop={scrollTop}
scrollTopBody={scrollTopBody}
selectedLgCatCd={selectedLgCatCd}
setSelectedLgCatCd={setSelectedLgCatCd}
/>

View File

@@ -1,15 +1,15 @@
import React, {
memo,
useCallback,
} from 'react';
import React, { memo, useCallback } from "react";
import classNames from 'classnames';
import classNames from "classnames";
import { useDispatch } from "react-redux";
import Spottable from '@enact/spotlight/Spottable';
import { setContainerLastFocusedElement } from '@enact/spotlight/src/container';
import Spottable from "@enact/spotlight/Spottable";
import { setContainerLastFocusedElement } from "@enact/spotlight/src/container";
import { SpotlightIds } from '../../../../utils/SpotlightIds';
import css from './OnSaleNavItem.module.less';
import { updatePanel } from "../../../../actions/panelActions";
import { panel_names } from "../../../../utils/Config";
import { SpotlightIds } from "../../../../utils/SpotlightIds";
import css from "./OnSaleNavItem.module.less";
const SpottableComponent = Spottable("li");
@@ -17,31 +17,38 @@ export default memo(function OnSaleNavItem({
categoryInfo,
index,
panelInfo,
scrollTop,
scrollTopBody,
selectedLgCatCd,
setSelectedLgCatCd,
...rest
}) {
const dispatch = useDispatch();
const { lgCatCd, lgCatNm } = categoryInfo;
const handleClick = useCallback(
(lgCatCd) => {
if (selectedLgCatCd === lgCatCd) {
return scrollTop();
return scrollTopBody();
}
dispatch(
updatePanel({
name: panel_names.ON_SALE_PANEL,
})
);
setContainerLastFocusedElement(null, [SpotlightIds.TBODY]);
setSelectedLgCatCd(lgCatCd);
scrollTop();
scrollTopBody();
},
[selectedLgCatCd]
[dispatch, selectedLgCatCd]
);
return (
<SpottableComponent
className={classNames(
css.item,
selectedLgCatCd
? selectedLgCatCd === lgCatCd && css.selected
: panelInfo?.lgCatCd

View File

@@ -1,30 +1,25 @@
import React, {
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
useDispatch,
useSelector,
} from 'react-redux';
import { useDispatch, useSelector } from "react-redux";
import Spotlight from '@enact/spotlight';
import Spotlight from "@enact/spotlight";
import { changeAppStatus } from '../../actions/commonActions';
import { getOnSaleInfo } from '../../actions/onSaleActions';
import TBody from '../../components/TBody/TBody';
import TButton, { TYPES } from '../../components/TButton/TButton';
import { removeDotAndColon } from '../../components/TItemCard/TItemCard';
import TPanel from '../../components/TPanel/TPanel';
import useScrollTo from '../../hooks/useScrollTo';
import OnSaleContents from './OnSaleContents/OnSaleContents';
import OnSaleNav from './OnSaleNav/OnSaleNav';
import css from './OnSalePanel.module.less';
import { changeAppStatus } from "../../actions/commonActions";
import { getOnSaleInfo } from "../../actions/onSaleActions";
import { updatePanel } from "../../actions/panelActions";
import TBody from "../../components/TBody/TBody";
import TButton, { TYPES } from "../../components/TButton/TButton";
import { removeDotAndColon } from "../../components/TItemCard/TItemCard";
import TPanel from "../../components/TPanel/TPanel";
import useScrollTo from "../../hooks/useScrollTo";
import { panel_names } from "../../utils/Config";
import OnSaleContents from "./OnSaleContents/OnSaleContents";
import OnSaleNav from "./OnSaleNav/OnSaleNav";
import css from "./OnSalePanel.module.less";
export default function OnSalePanel() {
const { getScrollTo, scrollTop } = useScrollTo();
const { getScrollTo: getScrollToBody, scrollTop: scrollTopBody } =
useScrollTo();
const dispatch = useDispatch();
@@ -41,12 +36,13 @@ export default function OnSalePanel() {
);
const [categories, setCategories] = useState();
const [selectedLgCatCd, setSelectedLgCatCd] = useState();
const [firstFocusableTarget, setFirstFocusableTarget] = useState();
const [isInitialRendered, setIsInitialRendered] = useState(true);
const [isMounted, setIsMounted] = useState(false);
const [isTopButtonClicked, setIsTopButtonClicked] = useState(false);
const [targetId, setTargetId] = useState();
const [selectedLgCatCd, setSelectedLgCatCd] = useState();
const timerRef = useRef();
const timeoutRef = useRef();
const isLoading = onSaleDataStatus !== "fulfilled";
@@ -72,86 +68,123 @@ export default function OnSalePanel() {
}, [dispatch, isInitialRendered]);
useEffect(() => {
if (categoryInfos) {
if (!categories) {
setCategories(categoryInfos);
} else {
const code = panelInfo?.lgCatCd
? panelInfo.lgCatCd
: categoryInfos[0]?.lgCatCd;
const focusedItem = document.getElementById(`spotlightId-${code}`);
focusedItem && Spotlight.focus(focusedItem);
}
if (categoryInfos && !categories) {
setCategories(categoryInfos);
}
}, [categories, categoryInfos]);
useEffect(() => {
if (selectedLgCatCd) {
let props = { lgCatCd: selectedLgCatCd, saleInfosIncFlag: "Y" };
dispatch(getOnSaleInfo(props));
dispatch(
getOnSaleInfo({ lgCatCd: selectedLgCatCd, saleInfosIncFlag: "Y" })
);
}
}, [dispatch, selectedLgCatCd]);
useEffect(() => {
if (saleInfos && !isInitialRendered) {
const prdtId = saleInfos[0].saleProductInfos[0].prdtId;
setTargetId("spotlightId-" + removeDotAndColon(prdtId));
setFirstFocusableTarget("spotlightId-" + removeDotAndColon(prdtId));
setIsMounted(true);
}
}, [saleInfos]);
useEffect(() => {
return () => clearTimeout(timerRef.current);
if (isMounted) {
const hasPanelInfo = Object.keys(panelInfo).length > 0;
let targetId;
if (hasPanelInfo) {
targetId = panelInfo?.targetId ?? "spotlightId-" + panelInfo?.lgCatCd;
} else {
targetId = "spotlightId-" + categoryInfos[0]?.lgCatCd;
}
const initialFocusTarget = document.querySelector(
`[data-spotlight-id="${targetId}"]`
);
initialFocusTarget && Spotlight.focus(initialFocusTarget);
}
}, [isMounted]);
useEffect(() => {
return () => {
const lastFocusedTarget = Spotlight.getCurrent();
if (lastFocusedTarget) {
const exprOrd = parseInt(
lastFocusedTarget.getAttribute("data-exposure-order")
);
const lgCatCd = lastFocusedTarget.getAttribute("data-lg-category-code");
const targetId = lastFocusedTarget.getAttribute("data-spotlight-id");
dispatch(
updatePanel({
name: panel_names.ON_SALE_PANEL,
panelInfo: { exprOrd, lgCatCd, targetId },
})
);
}
clearTimeout(timeoutRef.current);
};
}, []);
const handleTopButtonClick = useCallback(() => {
if (!targetId) {
return scrollTop();
if (!firstFocusableTarget) {
return scrollTopBody();
}
scrollTop();
scrollTopBody();
setIsTopButtonClicked(true);
timerRef.current = setTimeout(() => {
Spotlight.focus(targetId);
timeoutRef.current = setTimeout(() => {
Spotlight.focus(firstFocusableTarget);
setIsTopButtonClicked(false);
}, 0);
}, [targetId]);
}, [firstFocusableTarget]);
return (
<TPanel className={css.tPanel} id={"tPanel"}>
{categories && categories.length > 0 && (
<OnSaleNav
categoryInfos={categories}
panelInfo={panelInfo}
scrollTop={scrollTop}
selectedLgCatCd={selectedLgCatCd}
setSelectedLgCatCd={setSelectedLgCatCd}
/>
)}
<TBody className={css.tBody} cbScrollTo={getScrollTo}>
{saleInfos &&
saleInfos.map(({ saleNm, saleProductInfos }, index) => (
<OnSaleContents
index={index}
isTopButtonClicked={isTopButtonClicked}
key={"saleInfo-" + index}
saleNm={saleNm}
saleProductInfos={saleProductInfos}
scrollTop={scrollTop}
selectedLgCatCd={selectedLgCatCd}
/>
))}
{saleInfos && saleInfos.length > 1 && !isLoading && (
<TButton
onClick={handleTopButtonClick}
size={null}
type={TYPES.topButton}
{categories && (
<React.Fragment>
<OnSaleNav
categoryInfos={categories}
panelInfo={panelInfo}
scrollTopBody={scrollTopBody}
selectedLgCatCd={selectedLgCatCd}
setSelectedLgCatCd={setSelectedLgCatCd}
/>
)}
</TBody>
<TBody className={css.tBody} cbScrollTo={getScrollToBody}>
<div data-marker="scroll-marker" />
{saleInfos &&
saleInfos.map(({ saleNm, saleProductInfos }, index) => (
<OnSaleContents
index={index}
isTopButtonClicked={isTopButtonClicked}
key={"saleInfo-" + index}
saleNm={saleNm}
saleProductInfos={saleProductInfos}
scrollTopBody={scrollTopBody}
selectedLgCatCd={selectedLgCatCd}
/>
))}
{saleInfos && saleInfos.length > 1 && !isLoading && (
<TButton
onClick={handleTopButtonClick}
size={null}
type={TYPES.topButton}
/>
)}
</TBody>
</React.Fragment>
)}
</TPanel>
);
}