SidePanel.jsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import React from "react";
  2. import styled from "styled-components";
  3. import { useTranslation } from "react-i18next";
  4. import usePortal from "react-useportal";
  5. const Overlay = styled.div`
  6. position: fixed;
  7. left: 0;
  8. top: 0;
  9. bottom: 0;
  10. right: 0;
  11. background-color: rgba(0, 0, 0, 0.6);
  12. `;
  13. const StyledSidePanel = styled.div`
  14. position: fixed;
  15. ${({ position }) => (position === "right" ? "right: 0;" : "left: 0;")}
  16. top: 0;
  17. bottom: 0;
  18. z-index: ${({ modal }) => (modal ? 290 : 280)};
  19. display: flex;
  20. flex-direction: column;
  21. height: 100%;
  22. max-height: 100%;
  23. overflow: hidden;
  24. background-color: ${({ modal }) =>
  25. modal ? "var(--color-darkGrey)" : "var(--color-blueGrey)"};
  26. box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
  27. min-width: 280px;
  28. max-width: 500px;
  29. width: ${({ width }) => (width ? `${width}` : "25%")};
  30. overflow-y: auto;
  31. transform: translateX(100%);
  32. transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
  33. ${({ open, position }) => {
  34. let start = -100;
  35. let end = 0;
  36. if (position === "right") {
  37. start = 100;
  38. }
  39. return open
  40. ? `transform: translateX(${end}%);`
  41. : `transform: translateX(${start}%);`;
  42. }}
  43. ${({ open }) => (open ? "opacity: 1;" : "opacity: 0.2;")}
  44. & .close {
  45. position: fixed;
  46. top: 5px;
  47. right: 10px;
  48. }
  49. & .title {
  50. font-weight: 700;
  51. padding: 0.5em;
  52. margin: 0;
  53. }
  54. & > .content {
  55. flex: 1;
  56. overflow: auto;
  57. ${({ noMargin }) => (noMargin ? "" : "padding: 1em")};
  58. & header {
  59. padding: 0.5em;
  60. margin-top: 2em;
  61. background-color: var(--color-blueGrey);
  62. border-radius: 0.5em 0.5em 0em 0em;
  63. & h3 {
  64. margin: 0.2em 0.2em 0.2em;
  65. font-weight: 300;
  66. }
  67. }
  68. & header:first-child {
  69. margin-top: 0;
  70. }
  71. & section {
  72. border-radius: 0em 0em 0.5em 0.5em;
  73. padding: 1em;
  74. background-color: var(--color-darkBlueGrey);
  75. }
  76. }
  77. & footer {
  78. margin-top: 1em;
  79. }
  80. `;
  81. const SidePanel = ({
  82. children,
  83. position,
  84. noMargin,
  85. onClose = () => {},
  86. title,
  87. footer,
  88. show,
  89. open = show,
  90. modal = false,
  91. width,
  92. }) => {
  93. const { t } = useTranslation();
  94. const [bindTo] = React.useState(() =>
  95. document.getElementById(modal ? "modal-container" : "panel-container")
  96. );
  97. const { ref, Portal, openPortal, closePortal, isOpen } = usePortal({
  98. closeOnOutsideClick: modal,
  99. bindTo,
  100. });
  101. const onAnimationEnd = React.useCallback(() => {
  102. if (!isOpen) {
  103. onClose();
  104. }
  105. }, [isOpen, onClose]);
  106. React.useEffect(() => {
  107. if (open) {
  108. openPortal();
  109. } else {
  110. closePortal();
  111. }
  112. }, [openPortal, closePortal, open]);
  113. React.useEffect(() => {
  114. if (isOpen && modal) {
  115. document.getElementById("root").classList.add("blurry");
  116. } else {
  117. document.getElementById("root").classList.remove("blurry");
  118. }
  119. }, [isOpen, modal]);
  120. return (
  121. <Portal>
  122. {modal && isOpen && <Overlay onClick={closePortal} />}
  123. <StyledSidePanel
  124. position={position}
  125. open={isOpen}
  126. onTransitionEnd={onAnimationEnd}
  127. noMargin={noMargin}
  128. ref={ref}
  129. width={width}
  130. modal={modal}
  131. className={isOpen ? "side-panel open" : "side-panel"}
  132. >
  133. <header>
  134. {title && <h2 className="title">{title}</h2>}
  135. <button
  136. className="button clear icon-only close"
  137. onClick={closePortal}
  138. >
  139. <img
  140. src="https://icongr.am/feather/x.svg?size=42&color=ffffff"
  141. alt={t("Close")}
  142. />
  143. </button>
  144. </header>
  145. <div className="content">{open && children}</div>
  146. {footer && <footer>{footer}</footer>}
  147. </StyledSidePanel>
  148. </Portal>
  149. );
  150. };
  151. export default SidePanel;