sidebarPanel.tsx 3.5 KB

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