import {useEffect, useRef, useState} from 'react'; import styled from '@emotion/styled'; import {AnimatePresence} from 'framer-motion'; import {Button} from 'sentry/components/button'; import SlideOverPanel from 'sentry/components/slideOverPanel'; import {IconPanel} from 'sentry/icons'; import {IconClose} from 'sentry/icons/iconClose'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import localStorage from 'sentry/utils/localStorage'; import useKeyPress from 'sentry/utils/useKeyPress'; import useOnClickOutside from 'sentry/utils/useOnClickOutside'; type DetailProps = { children: React.ReactNode; detailKey?: string; onClose?: () => void; onOpen?: () => void; skipCloseOnOutsideClick?: boolean; startingPositionOnLoad?: 'right' | 'bottom'; }; type DetailState = { collapsed: boolean; }; const SLIDEOUT_STORAGE_KEY = 'starfish-panel-slideout-direction'; function isValidSlidePosition(value: string | null): value is 'right' | 'bottom' { return value === 'right' || value === 'bottom'; } export default function Detail({ children, detailKey, onClose, onOpen, startingPositionOnLoad, skipCloseOnOutsideClick = false, }: DetailProps) { const localStorageValue = localStorage.getItem(SLIDEOUT_STORAGE_KEY); const storedSlideOutPosition = isValidSlidePosition(localStorageValue) ? localStorageValue : null; const [state, setState] = useState({collapsed: true}); const [slidePosition, setSlidePosition] = useState<'right' | 'bottom'>( startingPositionOnLoad ?? storedSlideOutPosition ?? 'right' ); const escapeKeyPressed = useKeyPress('Escape'); // Any time the key prop changes (due to user interaction), we want to open the panel useEffect(() => { if (detailKey) { setState({collapsed: false}); } else { setState({collapsed: true}); } }, [detailKey]); const panelRef = useRef(null); useOnClickOutside(panelRef, () => { if (!state.collapsed && !skipCloseOnOutsideClick) { onClose?.(); setState({collapsed: true}); } }); useEffect(() => { if (escapeKeyPressed) { if (!state.collapsed) { onClose?.(); setState({collapsed: true}); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [escapeKeyPressed]); const handleDocking = (position: 'right' | 'bottom') => { setSlidePosition(position); localStorage.setItem(SLIDEOUT_STORAGE_KEY, position); }; return ( } onClick={() => handleDocking('bottom')} /> } onClick={() => handleDocking('right')} /> } onClick={() => { setState({collapsed: true}); onClose?.(); }} /> {children} ); } const CloseButton = styled(Button)` color: ${p => p.theme.gray300}; &:hover { color: ${p => p.theme.gray400}; } z-index: 100; `; const PanelButton = styled(Button)` color: ${p => p.theme.gray300}; &:hover { color: ${p => p.theme.gray400}; } z-index: 100; `; const CloseButtonWrapper = styled('div')` justify-content: flex-end; display: flex; padding: ${space(2)}; `; const DetailWrapper = styled('div')` padding: 0 ${space(4)} ${space(4)}; `;