ContextMenu.tsx 2.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import React from "react";
  2. import { Popover } from "./Popover";
  3. import { render, unmountComponentAtNode } from "react-dom";
  4. import "./ContextMenu.scss";
  5. type ContextMenuOption = {
  6. label: string;
  7. action(): void;
  8. };
  9. type Props = {
  10. options: ContextMenuOption[];
  11. onCloseRequest?(): void;
  12. top: number;
  13. left: number;
  14. };
  15. const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => {
  16. const isDarkTheme = !!document
  17. .querySelector(".excalidraw")
  18. ?.classList.contains("Appearance_dark");
  19. const wrapperClasses = `excalidraw ${
  20. isDarkTheme ? "Appearance_dark Appearance_dark-background-none" : ""
  21. }`;
  22. return (
  23. <div className={wrapperClasses}>
  24. <Popover
  25. onCloseRequest={onCloseRequest}
  26. top={top}
  27. left={left}
  28. fitInViewport={true}
  29. >
  30. <ul
  31. className="context-menu"
  32. onContextMenu={(event) => event.preventDefault()}
  33. >
  34. {options.map((option, idx) => (
  35. <li key={idx} onClick={onCloseRequest}>
  36. <ContextMenuOption {...option} />
  37. </li>
  38. ))}
  39. </ul>
  40. </Popover>
  41. </div>
  42. );
  43. };
  44. const ContextMenuOption = ({ label, action }: ContextMenuOption) => (
  45. <button className="context-menu-option" onClick={action}>
  46. {label}
  47. </button>
  48. );
  49. let contextMenuNode: HTMLDivElement;
  50. const getContextMenuNode = (): HTMLDivElement => {
  51. if (contextMenuNode) {
  52. return contextMenuNode;
  53. }
  54. const div = document.createElement("div");
  55. document.body.appendChild(div);
  56. return (contextMenuNode = div);
  57. };
  58. type ContextMenuParams = {
  59. options: (ContextMenuOption | false | null | undefined)[];
  60. top: number;
  61. left: number;
  62. };
  63. const handleClose = () => {
  64. unmountComponentAtNode(getContextMenuNode());
  65. };
  66. export default {
  67. push(params: ContextMenuParams) {
  68. const options = Array.of<ContextMenuOption>();
  69. params.options.forEach((option) => {
  70. if (option) {
  71. options.push(option);
  72. }
  73. });
  74. if (options.length) {
  75. render(
  76. <ContextMenu
  77. top={params.top}
  78. left={params.left}
  79. options={options}
  80. onCloseRequest={handleClose}
  81. />,
  82. getContextMenuNode(),
  83. );
  84. }
  85. },
  86. };