detailPanel.tsx 3.4 KB

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