sidebarPanel.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import {useCallback, useEffect, useRef} from 'react';
  2. import {createPortal} from 'react-dom';
  3. import {css} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import {IconClose} from 'sentry/icons';
  6. import HookStore from 'sentry/stores/hookStore';
  7. import {slideInLeft} from 'sentry/styles/animations';
  8. import {space} from 'sentry/styles/space';
  9. import type {CommonSidebarProps} from './types';
  10. type PositionProps = Pick<CommonSidebarProps, 'orientation' | 'collapsed'>;
  11. const PanelContainer = styled('div')<PositionProps>`
  12. position: fixed;
  13. bottom: 0;
  14. display: flex;
  15. flex-direction: column;
  16. background: ${p => p.theme.background};
  17. color: ${p => p.theme.textColor};
  18. border-right: 1px solid ${p => p.theme.border};
  19. box-shadow: 1px 0 2px rgba(0, 0, 0, 0.06);
  20. text-align: left;
  21. animation: 200ms ${slideInLeft};
  22. z-index: ${p => p.theme.zIndex.sidebar - 1};
  23. ${p =>
  24. p.orientation === 'top'
  25. ? css`
  26. top: ${p.theme.sidebar.mobileHeight};
  27. left: 0;
  28. right: 0;
  29. `
  30. : css`
  31. width: 460px;
  32. top: 0;
  33. left: ${p.collapsed
  34. ? p.theme.sidebar.collapsedWidth
  35. : p.theme.sidebar.expandedWidth};
  36. `};
  37. `;
  38. interface Props extends React.HTMLAttributes<HTMLDivElement> {
  39. collapsed: CommonSidebarProps['collapsed'];
  40. hidePanel: CommonSidebarProps['hidePanel'];
  41. orientation: CommonSidebarProps['orientation'];
  42. title?: string;
  43. }
  44. const getSidebarPortal = () => {
  45. let portal = document.getElementById('sidebar-flyout-portal');
  46. if (!portal) {
  47. portal = document.createElement('div');
  48. portal.setAttribute('id', 'sidebar-flyout-portal');
  49. document.body.appendChild(portal);
  50. }
  51. return portal as HTMLDivElement;
  52. };
  53. function SidebarPanel({
  54. orientation,
  55. collapsed,
  56. hidePanel,
  57. title,
  58. children,
  59. ...props
  60. }: Props): React.ReactElement {
  61. const portalEl = useRef<HTMLDivElement>(getSidebarPortal());
  62. const panelCloseHandler = useCallback(
  63. (evt: MouseEvent) => {
  64. if (!(evt.target instanceof Element)) {
  65. return;
  66. }
  67. if (portalEl.current.contains(evt.target)) {
  68. return;
  69. }
  70. // If we are in Sandbox, don't hide panel when the modal is clicked (before the email is added)
  71. const blockHideSidebar = HookStore.get('onboarding:block-hide-sidebar')[0]?.();
  72. if (blockHideSidebar) {
  73. return;
  74. }
  75. hidePanel();
  76. },
  77. [hidePanel]
  78. );
  79. useEffect(() => {
  80. // Wait one tick to setup the click handler so we don't detect the click
  81. // that is bubbling up from opening the panel itself
  82. window.setTimeout(() => document.addEventListener('click', panelCloseHandler));
  83. return function cleanup() {
  84. window.setTimeout(() => document.removeEventListener('click', panelCloseHandler));
  85. };
  86. }, [panelCloseHandler]);
  87. return createPortal(
  88. <PanelContainer
  89. role="dialog"
  90. collapsed={collapsed}
  91. orientation={orientation}
  92. {...props}
  93. >
  94. {title ? (
  95. <SidebarPanelHeader>
  96. <Title>{title}</Title>
  97. <PanelClose onClick={hidePanel} />
  98. </SidebarPanelHeader>
  99. ) : null}
  100. <SidebarPanelBody hasHeader={!!title}>{children}</SidebarPanelBody>
  101. </PanelContainer>,
  102. portalEl.current
  103. );
  104. }
  105. export default SidebarPanel;
  106. const SidebarPanelHeader = styled('div')`
  107. border-bottom: 1px solid ${p => p.theme.border};
  108. padding: ${space(3)};
  109. background: ${p => p.theme.background};
  110. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
  111. height: 60px;
  112. display: flex;
  113. justify-content: space-between;
  114. align-items: center;
  115. flex-shrink: 1;
  116. `;
  117. const SidebarPanelBody = styled('div')<{hasHeader: boolean}>`
  118. display: flex;
  119. flex-direction: column;
  120. flex-grow: 1;
  121. overflow: auto;
  122. position: relative;
  123. `;
  124. const PanelClose = styled(IconClose)`
  125. color: ${p => p.theme.subText};
  126. cursor: pointer;
  127. position: relative;
  128. padding: ${space(0.75)};
  129. &:hover {
  130. color: ${p => p.theme.textColor};
  131. }
  132. `;
  133. PanelClose.defaultProps = {
  134. size: 'lg',
  135. };
  136. const Title = styled('div')`
  137. font-size: ${p => p.theme.fontSizeExtraLarge};
  138. margin: 0;
  139. `;