detailPanel.tsx 4.0 KB

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