sidebarPanel.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import {useEffect, useRef} from 'react';
  2. import ReactDOM 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. type Props = React.ComponentProps<typeof PanelContainer> &
  38. Pick<CommonSidebarProps, 'collapsed' | 'orientation' | 'hidePanel'> & {
  39. title?: string;
  40. };
  41. /**
  42. * Get the container element of the sidebar that react portals into.
  43. */
  44. export const getSidebarPanelContainer = () =>
  45. document.getElementById('sidebar-flyout-portal') as HTMLDivElement;
  46. const makePortal = () => {
  47. const portal = document.createElement('div');
  48. portal.setAttribute('id', 'sidebar-flyout-portal');
  49. document.body.appendChild(portal);
  50. return portal;
  51. };
  52. function SidebarPanel({
  53. orientation,
  54. collapsed,
  55. hidePanel,
  56. title,
  57. children,
  58. ...props
  59. }: Props) {
  60. const portalEl = useRef<HTMLDivElement>(getSidebarPanelContainer() || makePortal());
  61. useEffect(() => {
  62. document.addEventListener('click', panelCloseHandler);
  63. return function cleanup() {
  64. document.removeEventListener('click', panelCloseHandler);
  65. };
  66. }, []);
  67. function panelCloseHandler(evt: MouseEvent) {
  68. if (!(evt.target instanceof Element)) {
  69. return;
  70. }
  71. const panel = getSidebarPanelContainer();
  72. if (panel?.contains(evt.target)) {
  73. return;
  74. }
  75. hidePanel();
  76. }
  77. const sidebar = (
  78. <PanelContainer
  79. role="dialog"
  80. collapsed={collapsed}
  81. orientation={orientation}
  82. {...props}
  83. >
  84. {title && (
  85. <SidebarPanelHeader>
  86. <Title>{title}</Title>
  87. <PanelClose onClick={hidePanel} />
  88. </SidebarPanelHeader>
  89. )}
  90. <SidebarPanelBody hasHeader={!!title}>{children}</SidebarPanelBody>
  91. </PanelContainer>
  92. );
  93. return ReactDOM.createPortal(sidebar, portalEl.current);
  94. }
  95. export default SidebarPanel;
  96. const SidebarPanelHeader = styled('div')`
  97. border-bottom: 1px solid ${p => p.theme.border};
  98. padding: ${space(3)};
  99. background: ${p => p.theme.background};
  100. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
  101. height: 60px;
  102. display: flex;
  103. justify-content: space-between;
  104. align-items: center;
  105. flex-shrink: 1;
  106. `;
  107. const SidebarPanelBody = styled('div')<{hasHeader: boolean}>`
  108. display: flex;
  109. flex-direction: column;
  110. flex-grow: 1;
  111. overflow: auto;
  112. position: relative;
  113. `;
  114. const PanelClose = styled(IconClose)`
  115. color: ${p => p.theme.subText};
  116. cursor: pointer;
  117. position: relative;
  118. padding: ${space(0.75)};
  119. &:hover {
  120. color: ${p => p.theme.textColor};
  121. }
  122. `;
  123. PanelClose.defaultProps = {
  124. size: 'lg',
  125. };
  126. const Title = styled('div')`
  127. font-size: ${p => p.theme.fontSizeExtraLarge};
  128. margin: 0;
  129. `;