Files
shoptime/com.twin.app.shoptime/src/components/TabLayout/TabLayout.jsx

934 lines
26 KiB
JavaScript

import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import classNames from "classnames";
import { useDispatch, useSelector } from "react-redux";
//아이콘
import { Job } from "@enact/core/util";
//enact
import Skinnable from "@enact/sandstone/Skinnable";
import Spotlight from "@enact/spotlight";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import { Cancelable } from "@enact/ui/Cancelable";
import shoptimeFullIconRuc from "../../../assets/images/icons/ic-lnb-logo-shoptime-ruc-white.png";
//이미지
import shoptimeFullIcon from "../../../assets/images/icons/ic-lnb-logo-shoptime@3x.png";
import { gnbOpened } from "../../actions/commonActions";
import { checkEnterThroughGNB, resetHomeInfo } from "../../actions/homeActions";
import { resetPanels } from "../../actions/panelActions";
import usePrevious from "../../hooks/usePrevious";
import useScrollTo from "../../hooks/useScrollTo";
import { panel_names } from "../../utils/Config";
import { SpotlightIds } from "../../utils/SpotlightIds";
import TScroller from "../TScroller/TScroller";
import CategoryIcon from "./iconComponents/CategoryIcon";
import FeaturedBrandIcon from "./iconComponents/FeaturedBrandIcon";
import HomeIcon from "./iconComponents/HomeIcon";
import HotPicksIcon from "./iconComponents/HotPicksIcon";
import MyPageIcon from "./iconComponents/MyPageIcon";
import OnSaleIcon from "./iconComponents/OnSaleIcon";
import SearchIcon from "./iconComponents/SearchIcon";
import TrendingNowIcon from "./iconComponents/TrendingNowIcon";
import TabItem from "./TabItem";
import TabItemSub from "./TabItemSub";
import css from "./TabLayout.module.less";
const Container = SpotlightContainerDecorator(
{ enterTo: "default-element" },
"div"
);
const MainContainer = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
);
const CancelableDiv = Cancelable(
{ modal: true, onCancel: "handleCancel" },
Skinnable(Container)
);
class TabMenuItem {
constructor(
icons = "",
title = "",
spotlightId,
path,
patncNm,
target,
id,
children = []
) {
this.icons = icons;
this.title = title;
this.spotlightId = spotlightId;
this.path = path;
this.target = target;
this.id = id;
this.patncNm = patncNm;
this.children = children.map(
(child) =>
new TabMenuItem(
child.icons,
child.title,
child.spotlightId,
child.path,
child.patncNm,
child.target,
child.id
)
);
}
hasChildren = () => {
return this.children.length > 0;
};
getChildren = () => {
return this.children;
};
}
const deActivateTabJabFunc = (func) => {
func();
};
let deActivateTabJob = new Job(deActivateTabJabFunc, 2000);
const clearPanelSwitching = (ref) => {
if (ref) {
ref.current = false;
}
};
let panelSwitchingJob = new Job(clearPanelSwitching, 500);
const COLLABSED_MAIN = 0;
const ACTIVATED_MAIN = 1;
const ACTIVATED_SUB = 2;
const EXTRA_AREA = 3;
const PANELS_HAS_TAB = [
panel_names.CATEGORY_PANEL,
panel_names.FEATURED_BRANDS_PANEL,
panel_names.HOME_PANEL,
panel_names.HOT_PICKS_PANEL,
panel_names.MY_PAGE_PANEL,
panel_names.ON_SALE_PANEL,
panel_names.SEARCH_PANEL,
panel_names.TRENDING_NOW_PANEL,
];
export default function TabLayout({ topPanelName, onTabActivated, panelInfo }) {
const { getScrollTo, scrollTop } = useScrollTo();
const dispatch = useDispatch();
const [mainExpanded, setMainExpanded] = useState(false);
const [mainSelectedIndex, setMainSelectedIndex] = useState(-1);
const [secondDepthReduce, setSecondDepthReduce] = useState(false);
const [lastFocusId, setLastFocusId] = useState(null);
const [selectedTitle, setSelectedTitle] = useState("");
const [selectedSubItemId, setSelectedSubItemId] = useState(null);
const [selectedSubIndex, setSelectedSubIndex] = useState(-1);
const [subTabLastFocusId, setSubTabLastFocusId] = useState(null);
const [tabs, setTabs] = useState([]);
const [tabFocused, setTabFocused] = useState([false, false, false]); //COLLABSED_MAIN, ACTIVATED_MAIN, ACTIVATED_SUB
const panelSwitching = useRef(null);
const cursorVisible = useSelector(
(state) => state.common.appStatus.cursorVisible
);
const cursorVisibleRef = usePrevious(cursorVisible);
const data = useSelector((state) => state.home.menuData?.data);
const panels = useSelector((state) => state.panels.panels);
const { loginUserData } = useSelector((state) => state.common.appStatus);
const menuItems = useSelector((state) => state.home.menuItems);
const webOSVersion = useSelector(
(state) => state.common.appStatus.webOSVersion
);
const httpHeader = useSelector((state) => state.common.httpHeader);
const broadcast = useSelector(
(state) => state.common.broadcast,
(newState) => newState?.type !== "deActivateTab" // 'deActivateTab'일 때만 리렌더링 허용
);
const deviceCountryCode = httpHeader["X-Device-Country"];
const mouseNavOpen = useRef(new Job((func) => func(), 1000));
const mouseMainEntered = useRef(false);
const scrollTopJobRef = useRef(new Job((func) => func(), 0));
const getMenuData = (type) => {
let result = [];
switch (type) {
case "GNB":
result =
data?.gnb &&
data.gnb.map((item) => ({
title: item.menuNm,
}));
break;
//카테고리
case 10500:
result =
data?.homeCategory &&
data.homeCategory.map((item) => ({
icons: CategoryIcon,
id: item.lgCatCd,
title: item.lgCatNm,
spotlightId: "spotlight_category",
target: [
{
name: panel_names.CATEGORY_PANEL,
panelInfo: {
lgCatNm: item.lgCatNm,
lgCatCd: item.lgCatCd,
COUNT: item.COUNT,
currentSpot: null,
dropDownTab: 0,
tab: 0,
focusedContainerId: null,
expsOrd: item?.expsOrd,
},
},
],
}));
break;
//브랜드
case 10300:
result =
data?.shortFeaturedBrands &&
data.shortFeaturedBrands.map((item) => ({
icons: FeaturedBrandIcon,
id: item.patnrId,
path: item.patncLogoPath,
patncNm: item.patncNm,
spotlightId: "spotlight_featuredbrand",
target: [
{
name: panel_names.FEATURED_BRANDS_PANEL,
panelInfo: { from: "gnb", patnrId: item.patnrId },
},
],
}));
break;
//
case 10600:
result = data.mypage
.map((item) => ({
icons: MyPageIcon,
id: item.menuId,
title: item.menuNm,
spotlightId: "spotlight_mypage",
target: [
{
name: panel_names.MY_PAGE_PANEL,
panelInfo: {
menuNm: item.menuNm,
menuOrd: item.menuOrd,
menuId: item.menuId,
},
},
],
}))
.filter((item) => {
if (!loginUserData.userNumber && item.title === "My Orders") {
return false;
}
if (
webOSVersion < "6.0" &&
(item.title === "My Orders" || item.title === "My Info")
) {
return false;
}
return true;
});
break;
case 10700:
result = [
{
icons: SearchIcon,
spotlightId: "spotlight_search",
target: [{ name: panel_names.SEARCH_PANEL }],
},
];
break;
case 10100:
result = [
{
icons: HomeIcon,
spotlightId: "spotlight_home",
target: [{ name: panel_names.HOME_PANEL }],
},
];
break;
case 10400:
result = [
{
icons: OnSaleIcon,
spotlightId: "spotlight_onsale",
target: [{ name: panel_names.ON_SALE_PANEL }],
},
];
break;
case 10150:
result = [
{
icons: TrendingNowIcon,
spotlightId: "spotlight_trendingnow",
target: [{ name: panel_names.TRENDING_NOW_PANEL }],
},
];
break;
case 10200:
result = [
{
icons: HotPicksIcon,
spotlightId: "spotlight_hotpicks",
target: [{ name: panel_names.HOT_PICKS_PANEL }],
},
];
break;
}
return result;
};
const dataDivide = useCallback(() => {
if (data) {
for (let i = 0; i < menuItems.length; i++) {
const currentKey = menuItems[i].menuId;
const menuInfo = getMenuData(currentKey || "GNB");
for (let j = 0; j < menuInfo.length; j++) {
if (![10600, 10500, 10300].includes(currentKey)) {
menuItems[i].target = menuInfo[j].target;
}
menuItems[i].spotlightId = menuInfo[j].spotlightId;
menuItems[i].icons = menuInfo[j].icons;
if ([10600, 10500, 10300].includes(currentKey)) {
menuItems[i].children = menuInfo.map((item) => ({
id: item.id,
title: item.title,
path: item.path,
patncNm: item.patncNm,
target: item.target,
spotlightId: `secondDepth-${item.id}`,
}));
}
}
}
}
if (menuItems) {
setTabs(makeTabmenu());
}
}, [menuItems, loginUserData]);
const makeTabmenu = useCallback(() => {
const t = [];
for (let i = 0; i < menuItems.length; i++) {
const tabmenu = new TabMenuItem(
menuItems[i].icons,
menuItems[i].title,
menuItems[i].spotlightId,
menuItems[i].path,
menuItems[i].patncNm,
menuItems[i].target,
menuItems[i].id,
menuItems[i].children
);
t.push(tabmenu);
}
return t;
}, [menuItems]);
useEffect(() => {
dataDivide();
}, [menuItems, loginUserData]);
const deActivateTab = useCallback(() => {
setTabFocused([false, false, false, false]);
setMainSelectedIndex(-1);
setMainExpanded(false);
dispatch(gnbOpened(false));
}, [dispatch]);
const onTabHasFocus = useCallback(
(type) => (event) => {
switch (type) {
case COLLABSED_MAIN: {
if (cursorVisibleRef.current) {
mouseNavOpen.current.start(() => {
setMainExpanded(true);
});
}
break;
}
case ACTIVATED_MAIN: {
if (!cursorVisibleRef.current) {
const parent = event.target.parentNode;
const children = parent.childNodes;
const index = Array.prototype.indexOf.call(children, event.target);
setMainExpanded(true);
setMainSelectedIndex(index - 1);
setSecondDepthReduce(false);
}
break;
}
case ACTIVATED_SUB: {
if (!cursorVisibleRef.current) {
setMainExpanded(true);
}
}
case EXTRA_AREA: {
if (cursorVisibleRef.current) {
return;
}
}
}
setTabFocused((prevState) => {
const prev = [...prevState];
prev[type] = true;
return prev;
});
},
[deActivateTab]
);
const onTabBlur = useCallback(
(type) => (event) => {
switch (type) {
case COLLABSED_MAIN: {
if (cursorVisibleRef.current) {
mouseNavOpen.current.stop();
}
break;
}
case ACTIVATED_MAIN: {
if (!cursorVisibleRef.current) {
setMainExpanded(false);
}
break;
}
case ACTIVATED_SUB: {
if (!cursorVisibleRef.current) {
}
break;
}
case EXTRA_AREA: {
if (cursorVisibleRef.current) {
deActivateTabJob.stop();
return;
}
}
}
setTabFocused((prevState) => {
const prev = [...prevState];
prev[type] = true;
return prev;
});
},
[]
);
const onFocus = useCallback(() => {
if (showSubTab) {
setSecondDepthReduce((prev) => !prev);
}
}, [showSubTab]);
const spotToPanelJob = useRef(
new Job(() => {
const node = document.querySelector(`[id="${SpotlightIds.TPANEL}"]`);
if (node) {
Spotlight.focus(node);
}
})
);
const spotToPanel = useCallback((delayed = false) => {
const node = document.querySelector(`[id="${SpotlightIds.TPANEL}"]`);
if (node) {
if (delayed) {
spotToPanelJob.current.start(() => {
Spotlight.focus(node);
});
} else {
Spotlight.focus(node);
}
}
}, []);
useEffect(() => {
return () => {
spotToPanelJob.current.stop();
};
}, []);
const handleNavigation = useCallback(
({ index, target }) => {
setMainSelectedIndex(index);
mouseMainEntered.current = false;
mouseNavOpen.current.stop();
// second depth가 있을 경우 클릭 시 expanded
if (!target) {
setMainExpanded(true);
}
//같은패널로 클릭이벤트가 호출될때
if (
Array.isArray(target) &&
target[0]?.name &&
panels[0]?.name &&
panels[0]?.name === target[0]?.name
) {
deActivateTab();
spotToPanel();
return;
}
// 홈패널일 경우
if (target && target[0]?.name === panel_names.HOME_PANEL) {
deActivateTab();
spotToPanel(true);
dispatch(resetPanels());
dispatch(checkEnterThroughGNB(true));
dispatch(resetHomeInfo());
return;
}
//그 외 나머지
if (target) {
deActivateTab();
dispatch(resetPanels(target));
panelSwitching.current = true;
panelSwitchingJob.start(panelSwitching);
spotToPanel();
}
dispatch(resetHomeInfo());
},
[deActivateTab, dispatch, panels]
);
const onClickSubItem = useCallback(
({ target, itemId }) => {
if (selectedSubItemId === itemId) {
deActivateTab();
spotToPanel();
return;
}
if (target) {
dispatch(resetPanels(target));
deActivateTab();
panelSwitching.current = true;
panelSwitchingJob.start(panelSwitching);
spotToPanel();
}
dispatch(resetHomeInfo());
},
[dispatch, deActivateTab, selectedSubItemId]
);
const onClickExtraArea = useCallback(
({ index, target }) => {
deActivateTabJob.startAfter(100, deActivateTab);
},
[deActivateTab]
);
const tabActivated = useMemo(() => {
return mainExpanded || mainSelectedIndex >= 0;
}, [mainExpanded, mainSelectedIndex]);
const logoImg = useMemo(() => {
if (deviceCountryCode === "RU") {
return shoptimeFullIconRuc;
} else return shoptimeFullIcon;
}, [deviceCountryCode]);
const showTab = useMemo(() => {
if (!topPanelName || PANELS_HAS_TAB.indexOf(topPanelName) >= 0) {
return true;
}
return false;
}, [topPanelName]);
const showSubTab = useMemo(() => {
if (
tabActivated &&
tabs[mainSelectedIndex] &&
tabs[mainSelectedIndex].hasChildren()
) {
return true; // 서브 탭이 있는 경우
}
return false; // 서브 탭이 없는 경우
}, [tabActivated, mainSelectedIndex, tabs]);
const backKeyHandler = useCallback(
(ev) => {
if (tabActivated) {
Spotlight.focus(SpotlightIds.HOME_TBODY);
const node = document.querySelector(`[id="${SpotlightIds.TBODY}"]`);
if (node) {
Spotlight.focus(node);
}
deActivateTab();
ev?.stopPropagation();
return true;
}
},
[tabActivated, deActivateTab]
);
// 메인탭 라스트 포커스
useEffect(() => {
if (tabActivated) {
dispatch(gnbOpened(true));
if (panels.length === 0) {
Spotlight.focus("spotlight_home");
return;
}
if (lastFocusId) {
Spotlight.focus(lastFocusId);
}
//
else {
if (!subTabLastFocusId) {
Spotlight.focus("spotlight_home");
}
}
}
if (onTabActivated) {
onTabActivated(tabActivated && showTab);
}
}, [dispatch, showTab, tabActivated]);
//서브탭 라스트 포커스 & 서브탭 초기화
useEffect(() => {
if (!panelInfo) {
setMainSelectedIndex(-1);
setLastFocusId("spotlight_home");
setSubTabLastFocusId(null);
return;
}
let subTarget;
// if panelInfo is not that of PlayerPanel
if (!panelInfo?.shptmBanrTpNm) {
// case: Category 2depth
if (
topPanelName === panel_names.CATEGORY_PANEL && //
panelInfo?.lgCatCd
) {
subTarget = panelInfo.lgCatCd;
}
// case: Featured Brands 2depth
else if (
topPanelName === panel_names.FEATURED_BRANDS_PANEL &&
panelInfo?.patnrId
) {
subTarget = panelInfo.patnrId;
}
// case: My Info 2depth
else if (
topPanelName === panel_names.MY_PAGE_PANEL &&
panelInfo?.menuId
) {
subTarget = panelInfo.menuId;
}
}
if (subTarget) {
setSelectedSubItemId(subTarget);
setSubTabLastFocusId(`secondDepth-${subTarget}`);
}
}, [panelInfo, topPanelName]);
// esc키로 Home으로 이동시 SubItemId 초기화
useEffect(() => {
if (topPanelName === null && selectedSubItemId) {
setSelectedSubItemId(null);
}
}, [panels, selectedSubItemId]);
useEffect(() => {
if (tabActivated && showSubTab && subTabLastFocusId !== null) {
Spotlight.focus(subTabLastFocusId);
}
}, [tabActivated, subTabLastFocusId, mainSelectedIndex]);
useEffect(() => {
const hasFeaturedBrands =
tabs[mainSelectedIndex]?.children[0]?.path !== undefined;
const SCROLL_OFFSET_INDEX = hasFeaturedBrands ? 8 : 9;
const y = hasFeaturedBrands ? 110 : 100;
if (selectedSubIndex >= 0) {
if (selectedSubIndex === 0) {
scrollTopJobRef.current.start(() => scrollTop({ y: 0 }));
return () => scrollTopJobRef.current.stop();
}
}
if (
selectedSubIndex > SCROLL_OFFSET_INDEX &&
tabs[mainSelectedIndex]?.children.length - 1 >= selectedSubIndex
) {
const targetScrollIndex = selectedSubIndex - SCROLL_OFFSET_INDEX;
scrollTopJobRef.current.start(() =>
scrollTop({ y: y * targetScrollIndex })
);
return () => scrollTopJobRef.current.stop();
}
}, [selectedSubIndex]);
// 1Depth > 2Depth로 넘어갈때 서브메뉴가 없을때만 ( 의존성배열 )
useEffect(() => {
if (tabActivated && lastFocusId && !showSubTab) {
Spotlight.focus(lastFocusId);
}
}, [panels, lastFocusId]);
useEffect(() => {
setSecondDepthReduce(false);
if (showSubTab) {
tabs[mainSelectedIndex]?.children.forEach((item) => {
if (item.path) {
setSecondDepthReduce(true);
}
});
}
}, [secondDepthReduce, showSubTab, mainSelectedIndex]);
//[SHOPTIME-2052] 사방향키로 GNB 이동 후 매직마우스로 변경시 이동되면서 포커싱 되었던 메뉴가 selected됨
useEffect(() => {
if (cursorVisible && !showSubTab) {
setMainSelectedIndex(-1);
}
}, [cursorVisible]);
useEffect(() => {
if (broadcast?.type === "deActivateTab") {
console.log("TabLayout deactivateTab by broadcast");
deActivateTab();
}
}, [broadcast]);
const onMainMouseEnter = useCallback(
(ev) => {
mouseMainEntered.current = true;
onTabHasFocus(COLLABSED_MAIN)(ev);
},
[onTabHasFocus]
);
const onMainMouseLeave = useCallback(
(ev) => {
mouseMainEntered.current = false;
onTabBlur(COLLABSED_MAIN)(ev);
},
[onTabBlur]
);
const onMainMouseMove = useCallback(
(ev) => {
if (!mouseMainEntered.current) {
onMainMouseEnter(ev);
}
},
[onMainMouseEnter]
);
const moveFocusToMainTab = useCallback(
(e) => {
if (e.key === "ArrowLeft" && showSubTab && lastFocusId) {
Spotlight.focus(lastFocusId);
}
},
[showSubTab, lastFocusId]
);
if (!showTab) {
return null;
}
return (
<div
className={classNames(css.tabLayoutWrap, !tabActivated && css.hide)}
id={SpotlightIds.TAB_LAYOUT}
>
{data && (
<>
{
<CancelableDiv
spotlightRestrict="self-only"
spotlightId="activatedMain"
className={classNames(css.expandedRootContainer)}
handleCancel={backKeyHandler}
id="tabLayout"
>
{/* expanded Main */}
<MainContainer
className={classNames(
css.mainTabContainer,
css.tabWrap,
mainExpanded && css.expanded
)}
onFocus={onTabHasFocus(ACTIVATED_MAIN)}
onBlur={onTabBlur(ACTIVATED_MAIN)}
onMouseMove={onMainMouseMove}
onMouseEnter={onMainMouseEnter}
onMouseLeave={onMainMouseLeave}
>
<h1 className={css.logo}>
<img
src={logoImg}
alt=""
className={classNames(
deviceCountryCode === "RU" && css.rucLogo
)}
/>
</h1>
{tabs.map((item, index) => (
<TabItem
{...item}
key={"tabitemExpanded" + index}
onFocus={onFocus}
spotlightId={item.spotlightId}
setLastFocusId={setLastFocusId}
onClick={handleNavigation}
deActivateTab={deActivateTab}
showSubTab={showSubTab}
icons={item.icons}
expanded={mainExpanded}
mainSelected={
(panels.length === 0 &&
item.spotlightId === "spotlight_home") ||
(panels[0]?.name === panel_names.PLAYER_PANEL &&
panels.length === 1 &&
item.spotlightId === "spotlight_home") ||
(Array.isArray(item.target) &&
item.target[0]?.name &&
panels[0]?.name === item.target[0]?.name)
}
selected={
Array.isArray(item.children[0]?.target) &&
item.children[0]?.target[0]?.name &&
panels[0]?.name === item.children[0]?.target?.[0].name
}
subTabTarget={item.children[index]?.target}
setSelectedTitle={setSelectedTitle}
title={item.title}
index={index}
opened={mainSelectedIndex === index}
tabActivated={tabActivated}
setSubTabLastFocusId={setSubTabLastFocusId}
length={tabs.length}
/>
))}
</MainContainer>
{/* Sub */}
{
<Container
spotlightId="activatedSub"
className={classNames(
css.tabWrap,
css.secondDepthLayout,
secondDepthReduce && css.secondDepthReduce,
!showSubTab && css.hide
)}
onFocus={onTabHasFocus(ACTIVATED_SUB)}
onMouseOver={onTabHasFocus(ACTIVATED_SUB)}
onBlur={onTabBlur(ACTIVATED_SUB)}
onMouseLeave={onTabBlur(ACTIVATED_SUB)}
onKeyDown={moveFocusToMainTab}
>
<TScroller
cbScrollTo={getScrollTo}
className={css.scrollWrap}
>
{showSubTab &&
tabs[mainSelectedIndex]?.children.map((item, index) => {
return (
<TabItemSub
{...item}
mainMenuTitle={
tabs && tabs[mainSelectedIndex]?.title
}
key={"tabitemSubmenu" + index}
spotlightId={item.spotlightId}
setLastFocusId={setSubTabLastFocusId}
onClick={onClickSubItem}
expanded={mainExpanded}
index={index}
isSubItem={true}
deActivateTab={deActivateTab}
title={item.title + "-sub"}
itemId={item.id}
path={item.path}
patncNm={item.patncNm}
showSubTab={showSubTab}
setSelectedTitle={setSelectedTitle}
setSelectedSubItemId={setSelectedSubItemId}
setSelectedSubIndex={setSelectedSubIndex}
selected={
Array.isArray(item.target) &&
item.target[0]?.panelInfo &&
panels[0]?.panelInfo === item.target[0]?.panelInfo
}
label={
index * 1 +
1 +
" of " +
tabs[mainSelectedIndex]?.children.length
}
/>
);
})}
</TScroller>
</Container>
}
{/* Extra Area*/}
{tabActivated && (
<Container
className={classNames(css.tabWrap, css.extraArea)}
onClick={onClickExtraArea}
onFocus={onTabHasFocus(EXTRA_AREA)}
onMouseOver={onTabHasFocus(EXTRA_AREA)}
onBlur={onTabBlur(EXTRA_AREA)}
onMouseLeave={onTabBlur(EXTRA_AREA)}
/>
)}
</CancelableDiv>
}
</>
)}
</div>
);
}