import * as React from 'react'; import { useEffect, useCallback, FC, useMemo, useRef, useState, RefObject, CSSProperties } from 'react'; import { style } from 'typestyle'; import { useStore } from '../../services/store'; import { overlayZIndex, ugcZIndex } from '../consts'; import { IMedia } from '../../types'; import ShopThisLookProductList from '../ShopThisLook/ProductList'; import ShopThisLookModal from '../ShopThisLook/Modal'; import MobilePlayer from './Player/MobilePlayer'; import DesktopPlayer from './Player/DesktopPlayer'; import useDialogFocusTrap from '../../hooks/useDialogFocusTrap'; import useActiveThumbnailRect from '../../hooks/useActiveThumbnailRect'; import ThumbnailPills, { thumbnailPillsHoverClass } from '../ThumbnailPills'; interface PostViewerProps { onPostChange: (post: IMedia | null) => void; } const PostViewer: FC = ({ onPostChange }) => { const store = useStore(); const dialogRef = useRef(null); const dialogTitleId = useMemo( () => `${store.config.classPrefix || 'adl'}-postviewer-title`, [store.config.classPrefix], ); useDialogFocusTrap({ active: Boolean(store.post), containerRef: dialogRef }); // Event handlers const handleClose = useCallback(() => { store.setStoreState((prevState) => ({ ...prevState, post: null })); window.history.replaceState(null, '', null); }, [store]); // Basic keyboard navigation useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') handleClose(); }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [handleClose]); // (Focus trap handled by useDialogFocusTrap) if (!store.post) return null; // Determine what to show: // - On mobile: use MobilePlayer by default // - On desktop with product_list mode + display_desktop_player flag: use DesktopPlayer // - On desktop with product_list mode: show the product list component // - On mobile when shop_this_look_display is 'modal': show the product list component // - Otherwise: show the modal component const showMobilePlayer = store.isMobile && (store.data.settings.mobile_player_display !== false); const showDesktopPlayer = !showMobilePlayer && !store.isMobile && store.data.settings.shop_this_look_display === 'product_list' && store.data.settings.display_desktop_player === true; const showProductList = (!showMobilePlayer && !showDesktopPlayer && store.data.settings.shop_this_look_display === 'product_list') || (store.isMobile && store.data.settings.shop_this_look_display === 'modal'); return (
); }; /** * Renders a fixed-position clone of the active thumbnail in the portal, * sitting between the overlay (z-index 999) and the product list (z-index 1003). * * This solves a stacking context issue: the real thumbnail lives in the widget DOM * which may be trapped in a client-page stacking context. By rendering a visual copy * at the body level (via the portal), it reliably appears above the overlay. * * Position and image are synced via direct DOM mutation in a RAF loop (no React * state), so the clone moves in the same frame as the carousel. */ const ActiveThumbnailHighlight: FC<{ post: IMedia; classPrefix: string }> = ({ post, classPrefix, }) => { const store = useStore(); const cloneRef = useRef(null); const videoRef = useRef(null); const [isMuted, setIsMuted] = useState(true); useActiveThumbnailRect(cloneRef as RefObject, post); useEffect(() => { // reload the video when the post changes if (videoRef.current) { videoRef.current.load(); } }, [post.video]); const showMute = post.type === 'video' && !!post.video; const showAttribution = store.data.settings.show_attribution ?? true; return ( ); }; const styles = { cloneDefaults: style({ backgroundSize: 'cover', backgroundPosition: 'center', }), overlay: style({ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0, 0, 0, 0.5)', zIndex: overlayZIndex, }), srOnly: { position: 'absolute', width: 1, height: 1, padding: 0, margin: -1, overflow: 'hidden', clip: 'rect(0, 0, 0, 0)', whiteSpace: 'nowrap', border: 0, } as CSSProperties, }; export default PostViewer;