123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156 |
- import {useCallback, useEffect, useRef} from 'react';
- import {createPortal} from 'react-dom';
- import {css} from '@emotion/react';
- import styled from '@emotion/styled';
- import {IconClose} from 'sentry/icons';
- import {slideInLeft} from 'sentry/styles/animations';
- import space from 'sentry/styles/space';
- import {CommonSidebarProps} from './types';
- type PositionProps = Pick<CommonSidebarProps, 'orientation' | 'collapsed'>;
- const PanelContainer = styled('div')<PositionProps>`
- position: fixed;
- bottom: 0;
- display: flex;
- flex-direction: column;
- background: ${p => p.theme.background};
- color: ${p => p.theme.textColor};
- border-right: 1px solid ${p => p.theme.border};
- box-shadow: 1px 0 2px rgba(0, 0, 0, 0.06);
- text-align: left;
- animation: 200ms ${slideInLeft};
- z-index: ${p => p.theme.zIndex.sidebar - 1};
- ${p =>
- p.orientation === 'top'
- ? css`
- top: ${p.theme.sidebar.mobileHeight};
- left: 0;
- right: 0;
- `
- : css`
- width: 460px;
- top: 0;
- left: ${p.collapsed
- ? p.theme.sidebar.collapsedWidth
- : p.theme.sidebar.expandedWidth};
- `};
- `;
- interface Props extends React.HTMLAttributes<HTMLDivElement> {
- collapsed: CommonSidebarProps['collapsed'];
- hidePanel: CommonSidebarProps['hidePanel'];
- orientation: CommonSidebarProps['orientation'];
- title?: string;
- }
- const getSidebarPortal = () => {
- let portal = document.getElementById('sidebar-flyout-portal');
- if (!portal) {
- portal = document.createElement('div');
- portal.setAttribute('id', 'sidebar-flyout-portal');
- document.body.appendChild(portal);
- }
- return portal as HTMLDivElement;
- };
- function SidebarPanel({
- orientation,
- collapsed,
- hidePanel,
- title,
- children,
- ...props
- }: Props): React.ReactElement {
- const portalEl = useRef<HTMLDivElement>(getSidebarPortal());
- const panelCloseHandler = useCallback(
- (evt: MouseEvent) => {
- if (!(evt.target instanceof Element)) {
- return;
- }
- if (portalEl.current.contains(evt.target)) {
- return;
- }
- hidePanel();
- },
- [hidePanel]
- );
- useEffect(() => {
- // Wait one tick to setup the click handler so we don't detect the click
- // that is bubbling up from opening the panel itself
- window.setTimeout(() => document.addEventListener('click', panelCloseHandler));
- return function cleanup() {
- window.setTimeout(() => document.removeEventListener('click', panelCloseHandler));
- };
- }, [panelCloseHandler]);
- return createPortal(
- <PanelContainer
- role="dialog"
- collapsed={collapsed}
- orientation={orientation}
- {...props}
- >
- {title ? (
- <SidebarPanelHeader>
- <Title>{title}</Title>
- <PanelClose onClick={hidePanel} />
- </SidebarPanelHeader>
- ) : null}
- <SidebarPanelBody hasHeader={!!title}>{children}</SidebarPanelBody>
- </PanelContainer>,
- portalEl.current
- );
- }
- export default SidebarPanel;
- const SidebarPanelHeader = styled('div')`
- border-bottom: 1px solid ${p => p.theme.border};
- padding: ${space(3)};
- background: ${p => p.theme.background};
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
- height: 60px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-shrink: 1;
- `;
- const SidebarPanelBody = styled('div')<{hasHeader: boolean}>`
- display: flex;
- flex-direction: column;
- flex-grow: 1;
- overflow: auto;
- position: relative;
- `;
- const PanelClose = styled(IconClose)`
- color: ${p => p.theme.subText};
- cursor: pointer;
- position: relative;
- padding: ${space(0.75)};
- &:hover {
- color: ${p => p.theme.textColor};
- }
- `;
- PanelClose.defaultProps = {
- size: 'lg',
- };
- const Title = styled('div')`
- font-size: ${p => p.theme.fontSizeExtraLarge};
- margin: 0;
- `;
|