import * as React from 'react'; import { FC, useState, useCallback, useRef, useEffect, forwardRef, useImperativeHandle } from 'react'; import { keyframes, style } from 'typestyle'; import { useStore } from '../../../services/store'; import { ISettings, IMedia, Direction } from '../../../types'; import { productListButtonsZIndex, productListZIndex } from '../../consts'; import ProductList from '../../PostViewer/components/Products/ProductList'; import Content from '../../PostViewer/components/Content'; import CloseIcon from '../../Icons/Close'; import Arrow from '../../Icons/Arrows/Arrow'; import useBorderRadius from '../../../hooks/useBorderRadius'; import useAccessibilityMessage from '../../../hooks/useAccessibilityMessage'; import { getContentAnnouncementMessage, getSrOnlyStyle, } from '../../Layouts/Carousel/shared/accessibility'; import AttributionFooter from '../../PostViewer/components/AttributionFooter'; interface ListProps { settings: ISettings; classPrefix: string; onClose: () => void; post: IMedia; totalPosts: number; onPostChange: (post: IMedia | null) => void; /** When true, renders inline (position: relative, full-height) instead of fixed/animated. Used by DesktopPlayer. */ inline?: boolean; } export interface ShopThisLookProductListHandle { close: () => void; } const ShopThisLookProductList = forwardRef(( { settings, classPrefix, onClose, post, totalPosts, onPostChange, inline = false, }, ref, ) => { const store = useStore(); const containerRef = useRef(null); const contentRef = useRef(null); const { accessibilityMessage, announce } = useAccessibilityMessage(); const { isMobile } = store; const [isClosing, setIsClosing] = useState(false); const [containerDimensions, setContainerDimensions] = useState({ width: 0, height: 0 }); const borderRadius = useBorderRadius( containerDimensions.width, containerDimensions.height, settings.thumbnail_corners, isMobile ? 'top' : 'left', ); const isShopThisLookEnabled = settings.shop_this_look_display !== 'none'; const showAttribution = settings.show_attribution !== false; // default to true // Announce content details when product list opens or post changes useEffect(() => { announce(getContentAnnouncementMessage(post, post.postIndex, totalPosts)); }, [post.id, totalPosts, announce]); // Announce whenever the post changes const handleClose = useCallback(() => { setIsClosing(true); setTimeout(onClose, 300); }, [onClose]); useImperativeHandle(ref, () => ({ close: handleClose }), [handleClose]); const handleArrowClick = (dir: Direction) => { const newIndex = dir === 'left' ? post.postIndex - 1 : post.postIndex + 1; // Check if the new index is within bounds in order to handle clone posts for ProductList's infinite scroll let validIndex = newIndex; if (validIndex < 0) { // If the new index is less than 0, set it to the last post validIndex = totalPosts - 1; } else if (validIndex >= totalPosts) { // If the new index is greater than or equal to totalPosts, set it to 0 validIndex = 0; } const newPost = store.data.content.medias[validIndex]; store.setStoreState((state) => ({ ...state, currentPosition: validIndex, })); onPostChange(newPost); // Announcement handled by useEffect when post changes }; // observe container dimensions to adapt borderradius useEffect(() => { if (!containerRef.current) return; const resizeObserver = new ResizeObserver((entries) => { const { width, height } = entries[0].contentRect; setContainerDimensions({ width, height }); }); resizeObserver.observe(containerRef.current); // cleanup // eslint-disable-next-line consistent-return return () => resizeObserver.disconnect(); }, []); return (
{accessibilityMessage}
handleArrowClick('left')} show={!isMobile && !inline} classPrefix={classPrefix} size='compact' ariaLabel='Previous post' /> handleArrowClick('right')} show={!isMobile && !inline} classPrefix={classPrefix} size='compact' ariaLabel='Next post' /> {!inline ? ( ) : null}
{isShopThisLookEnabled ? (
) : (
)} {showAttribution ? ( ) : null}
); }); const styles = { container: ( isMobile: boolean, shopThisLookEnabled: boolean, isClosing: boolean, borderRadius: string, inline: boolean = false, ) => { const slideUp = keyframes({ from: { transform: 'translateY(100%)' }, to: { transform: 'translateY(0)' }, }); const slideDown = keyframes({ from: { transform: 'translateY(0)' }, to: { transform: 'translateY(100%)' }, }); const slideRight = keyframes({ from: { transform: 'translateX(100%)' }, to: { transform: 'translateX(0)' }, }); const slideLeft = keyframes({ from: { transform: 'translateX(0)' }, to: { transform: 'translateX(100%)' }, }); if (inline) { return style({ position: 'relative', height: '100%', width: '100%', display: 'flex', flexDirection: 'column', backgroundColor: '#FFFFFF', borderRadius, zIndex: productListZIndex, overflow: 'hidden', }); } return style({ position: 'fixed', bottom: 0, right: 0, height: isMobile ? (shopThisLookEnabled ? 'auto' : '70vh') : '100%', maxHeight: isMobile && shopThisLookEnabled ? '57vh' : undefined, width: isMobile ? '100%' : '20%', display: 'flex', flexDirection: 'column', backgroundColor: '#FFFFFF', borderRadius, zIndex: productListZIndex, ...(isMobile ? { transform: 'translateY(100%)', animation: isClosing ? `${slideDown} 0.3s ease-in forwards` : `${slideUp} 0.3s ease-out forwards`, } : { transform: 'translateX(100%)', animation: isClosing ? `${slideLeft} 0.3s ease-in forwards` : `${slideRight} 0.3s ease-out forwards`, }), }); }, actionBar: style({ height: '7vh', width: '100%', display: 'flex', justifyContent: 'flex-end', alignItems: 'center', }), actionButtons: style({ display: 'flex', alignItems: 'center', zIndex: productListButtonsZIndex, }), closeButton: style({ margin: '12px', cursor: 'pointer', background: 'none', border: 'none', padding: 0, }), content: (isShopThisLook: boolean) => style({ maxHeight: isShopThisLook ? '50%' : '100%', width: '100%', overflow: 'hidden', display: 'flex', flexDirection: 'column', }), productList: (isMobile: boolean) => style({ flex: isMobile ? '0 1 auto' : 1, minHeight: 0, width: '100%', overflow: isMobile ? 'auto' : 'hidden', }), }; export default ShopThisLookProductList;