editor-context-menu.tsx 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. import { FC, Fragment, memo, useEffect, useRef } from 'react'
  2. import ReactDOM from 'react-dom'
  3. import { getTooltip } from '@codemirror/view'
  4. import {
  5. Dropdown,
  6. DropdownMenu,
  7. DropdownItem,
  8. DropdownDivider,
  9. } from '@/shared/components/dropdown/dropdown-menu'
  10. import {
  11. useCodeMirrorStateContext,
  12. useCodeMirrorViewContext,
  13. } from './codemirror-context'
  14. import { contextMenuStateField } from '../extensions/context-menu'
  15. import { useFeatureFlag } from '@/shared/context/split-test-context'
  16. import { useContextMenuItems } from '../hooks/use-context-menu-items'
  17. import DropdownListItem from '@/shared/components/dropdown/dropdown-list-item'
  18. import { EditorContextMenuFeedback } from './editor-context-menu-feedback'
  19. import { sendContextMenuEvent } from '../utils/context-menu-analytics'
  20. const EditorContextMenu: FC = () => {
  21. const state = useCodeMirrorStateContext()
  22. const view = useCodeMirrorViewContext()
  23. const editorContextMenuEnabled = useFeatureFlag('editor-context-menu')
  24. const menuState = state.field(contextMenuStateField, false)
  25. if (!editorContextMenuEnabled || !menuState?.tooltip) {
  26. return null
  27. }
  28. const tooltipView = getTooltip(view, menuState.tooltip)
  29. if (!tooltipView) {
  30. return null
  31. }
  32. return ReactDOM.createPortal(<EditorContextMenuContent />, tooltipView.dom)
  33. }
  34. const EditorContextMenuContent: FC = memo(function EditorContextMenuContent() {
  35. const { menuItems, closeMenu, onToggle } = useContextMenuItems()
  36. const menuRef = useRef<any>(null)
  37. useEffect(() => {
  38. sendContextMenuEvent('menu-expand', {
  39. location: 'editor-context-menu',
  40. })
  41. menuRef.current?.focus()
  42. }, [])
  43. return (
  44. <Dropdown show onToggle={onToggle}>
  45. <div onContextMenu={event => event.preventDefault()}>
  46. <DropdownMenu
  47. ref={menuRef}
  48. show
  49. tabIndex={0}
  50. className="dropdown-menu-unpositioned"
  51. onKeyDown={event => {
  52. switch (event.code) {
  53. case 'Escape':
  54. case 'Tab':
  55. event.preventDefault()
  56. closeMenu()
  57. break
  58. }
  59. }}
  60. >
  61. {menuItems.map((menuItem, index) => (
  62. <Fragment key={index}>
  63. {menuItem.separatorAbove && <DropdownDivider />}
  64. <DropdownListItem>
  65. <DropdownItem
  66. as="button"
  67. onClick={() => menuItem.handler()}
  68. disabled={menuItem.disabled}
  69. trailingIcon={
  70. menuItem.shortcut ? (
  71. <span>{menuItem.shortcut}</span>
  72. ) : undefined
  73. }
  74. >
  75. {menuItem.label}
  76. </DropdownItem>
  77. </DropdownListItem>
  78. </Fragment>
  79. ))}
  80. <EditorContextMenuFeedback />
  81. </DropdownMenu>
  82. </div>
  83. </Dropdown>
  84. )
  85. })
  86. export default EditorContextMenu