123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154 |
- 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<DetailState>({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<HTMLDivElement>(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 (
- <AnimatePresence>
- <SlideOverPanel
- slidePosition={slidePosition}
- collapsed={state.collapsed}
- ref={panelRef}
- onOpen={onOpen}
- >
- <CloseButtonWrapper>
- <PanelButton
- priority="link"
- size="zero"
- borderless
- aria-label={t('Dock to the bottom')}
- disabled={slidePosition === 'bottom'}
- icon={<IconPanel size="sm" direction="down" />}
- onClick={() => handleDocking('bottom')}
- />
- <PanelButton
- priority="link"
- size="zero"
- borderless
- aria-label={t('Dock to the right')}
- disabled={slidePosition === 'right'}
- icon={<IconPanel size="sm" direction="right" />}
- onClick={() => handleDocking('right')}
- />
- <CloseButton
- priority="link"
- size="zero"
- borderless
- aria-label={t('Close Details')}
- icon={<IconClose size="sm" />}
- onClick={() => {
- setState({collapsed: true});
- onClose?.();
- }}
- />
- </CloseButtonWrapper>
- <DetailWrapper>{children}</DetailWrapper>
- </SlideOverPanel>
- </AnimatePresence>
- );
- }
- 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)};
- `;
|