sidebarPanel.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import React 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. z-index: ${p => p.theme.zIndex.sidebarPanel};
  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. ${p =>
  23. p.orientation === 'top'
  24. ? css`
  25. top: ${p.theme.sidebar.mobileHeight};
  26. left: 0;
  27. right: 0;
  28. `
  29. : css`
  30. width: 360px;
  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');
  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. class SidebarPanel extends React.Component<Props> {
  53. constructor(props: Props) {
  54. super(props);
  55. this.portalEl = getSidebarPanelContainer() || makePortal();
  56. }
  57. componentDidMount() {
  58. document.addEventListener('click', this.panelCloseHandler);
  59. }
  60. componentWillUnmount() {
  61. document.removeEventListener('click', this.panelCloseHandler);
  62. }
  63. portalEl: Element;
  64. panelCloseHandler = (evt: MouseEvent) => {
  65. if (!(evt.target instanceof Element)) {
  66. return;
  67. }
  68. const panel = getSidebarPanelContainer();
  69. if (panel?.contains(evt.target)) {
  70. return;
  71. }
  72. this.props.hidePanel();
  73. };
  74. render() {
  75. const {orientation, collapsed, hidePanel, title, children, ...props} = this.props;
  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, this.portalEl);
  88. }
  89. }
  90. export default SidebarPanel;
  91. const SidebarPanelHeader = styled('div')`
  92. border-bottom: 1px solid ${p => p.theme.border};
  93. padding: ${space(3)};
  94. background: ${p => p.theme.background};
  95. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
  96. height: 60px;
  97. display: flex;
  98. justify-content: space-between;
  99. align-items: center;
  100. flex-shrink: 1;
  101. `;
  102. const SidebarPanelBody = styled('div')<{hasHeader: boolean}>`
  103. display: flex;
  104. flex-direction: column;
  105. flex-grow: 1;
  106. overflow: auto;
  107. position: relative;
  108. `;
  109. const PanelClose = styled(IconClose)`
  110. color: ${p => p.theme.subText};
  111. cursor: pointer;
  112. position: relative;
  113. padding: ${space(0.75)};
  114. &:hover {
  115. color: ${p => p.theme.textColor};
  116. }
  117. `;
  118. PanelClose.defaultProps = {
  119. size: 'lg',
  120. };
  121. const Title = styled('div')`
  122. font-size: ${p => p.theme.fontSizeExtraLarge};
  123. margin: 0;
  124. `;