sidebarPanel.tsx 3.7 KB

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