detailPanel.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import {useEffect, useRef, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Button} from 'sentry/components/button';
  4. import {IconPanel} from 'sentry/icons';
  5. import {IconClose} from 'sentry/icons/iconClose';
  6. import {t} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import useKeyPress from 'sentry/utils/useKeyPress';
  9. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  10. import useOnClickOutside from 'sentry/utils/useOnClickOutside';
  11. import SlideOverPanel from 'sentry/views/starfish/components/slideOverPanel';
  12. type DetailProps = {
  13. children: React.ReactNode;
  14. detailKey?: string;
  15. onClose?: () => void;
  16. onOpen?: () => void;
  17. skipCloseOnOutsideClick?: boolean;
  18. };
  19. type DetailState = {
  20. collapsed: boolean;
  21. };
  22. const SLIDEOUT_STORAGE_KEY = 'starfish-panel-slideout-direction';
  23. export default function Detail({
  24. children,
  25. detailKey,
  26. onClose,
  27. onOpen,
  28. skipCloseOnOutsideClick = false,
  29. }: DetailProps) {
  30. const [state, setState] = useState<DetailState>({collapsed: true});
  31. const [slidePosition, setSlidePosition] = useLocalStorageState<'right' | 'bottom'>(
  32. SLIDEOUT_STORAGE_KEY,
  33. 'right'
  34. );
  35. const escapeKeyPressed = useKeyPress('Escape');
  36. // Any time the key prop changes (due to user interaction), we want to open the panel
  37. useEffect(() => {
  38. if (detailKey) {
  39. setState({collapsed: false});
  40. } else {
  41. setState({collapsed: true});
  42. }
  43. }, [detailKey]);
  44. const panelRef = useRef<HTMLDivElement>(null);
  45. useOnClickOutside(panelRef, () => {
  46. if (!state.collapsed && !skipCloseOnOutsideClick) {
  47. onClose?.();
  48. setState({collapsed: true});
  49. }
  50. });
  51. useEffect(() => {
  52. if (escapeKeyPressed) {
  53. if (!state.collapsed) {
  54. onClose?.();
  55. setState({collapsed: true});
  56. }
  57. }
  58. // eslint-disable-next-line react-hooks/exhaustive-deps
  59. }, [escapeKeyPressed]);
  60. return (
  61. <SlideOverPanel
  62. slidePosition={slidePosition}
  63. collapsed={state.collapsed}
  64. ref={panelRef}
  65. onOpen={onOpen}
  66. >
  67. <CloseButtonWrapper>
  68. <PanelButton
  69. priority="link"
  70. size="zero"
  71. borderless
  72. aria-label={t('Dock to the bottom')}
  73. disabled={slidePosition === 'bottom'}
  74. icon={<IconPanel size="sm" direction="down" />}
  75. onClick={() => {
  76. setSlidePosition('bottom');
  77. }}
  78. />
  79. <PanelButton
  80. priority="link"
  81. size="zero"
  82. borderless
  83. aria-label={t('Dock to the right')}
  84. disabled={slidePosition === 'right'}
  85. icon={<IconPanel size="sm" direction="right" />}
  86. onClick={() => {
  87. setSlidePosition('right');
  88. }}
  89. />
  90. <CloseButton
  91. priority="link"
  92. size="zero"
  93. borderless
  94. aria-label={t('Close Details')}
  95. icon={<IconClose size="sm" />}
  96. onClick={() => {
  97. setState({collapsed: true});
  98. onClose?.();
  99. }}
  100. />
  101. </CloseButtonWrapper>
  102. <DetailWrapper>{children}</DetailWrapper>
  103. </SlideOverPanel>
  104. );
  105. }
  106. const CloseButton = styled(Button)`
  107. color: ${p => p.theme.gray300};
  108. &:hover {
  109. color: ${p => p.theme.gray400};
  110. }
  111. z-index: 100;
  112. `;
  113. const PanelButton = styled(Button)`
  114. color: ${p => p.theme.gray300};
  115. &:hover {
  116. color: ${p => p.theme.gray400};
  117. }
  118. z-index: 100;
  119. `;
  120. const CloseButtonWrapper = styled('div')`
  121. justify-content: flex-end;
  122. display: flex;
  123. padding: ${space(2)};
  124. `;
  125. const DetailWrapper = styled('div')`
  126. padding: 0 ${space(4)};
  127. `;