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 (