file-tree-context-menu.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import React, { useCallback, useEffect, useRef } from 'react'
  2. import ReactDOM from 'react-dom'
  3. import {
  4. Dropdown,
  5. DropdownMenu,
  6. } from '@/shared/components/dropdown/dropdown-menu'
  7. import { useFileTreeData } from '@/shared/context/file-tree-data-context'
  8. import { useFileTreeMainContext } from '../contexts/file-tree-main'
  9. import FileTreeItemMenuItems from './file-tree-item/file-tree-item-menu-items'
  10. import classNames from 'classnames'
  11. function FileTreeContextMenu() {
  12. const { fileTreeReadOnly } = useFileTreeData()
  13. const { contextMenuCoords, setContextMenuCoords } = useFileTreeMainContext()
  14. const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
  15. const keyboardInputRef = useRef(false)
  16. useEffect(() => {
  17. if (contextMenuCoords) {
  18. toggleButtonRef.current = document.querySelector(
  19. '.entity-menu-toggle'
  20. ) as HTMLButtonElement | null
  21. }
  22. }, [contextMenuCoords])
  23. useEffect(() => {
  24. if (contextMenuCoords && keyboardInputRef.current) {
  25. const firstDropdownMenuItem = document.querySelector(
  26. '#dropdown-file-tree-context-menu .dropdown-item:not([disabled])'
  27. ) as HTMLButtonElement | null
  28. if (firstDropdownMenuItem) {
  29. firstDropdownMenuItem.focus()
  30. }
  31. }
  32. }, [contextMenuCoords])
  33. function close() {
  34. if (!contextMenuCoords) return
  35. setContextMenuCoords(null)
  36. if (toggleButtonRef.current) {
  37. // A11y - Focus moves back to the trigger button when the context menu is dismissed
  38. toggleButtonRef.current.focus()
  39. }
  40. }
  41. function handleToggle(wantOpen: boolean) {
  42. if (!wantOpen) close()
  43. }
  44. function handleClose(event: React.KeyboardEvent<Element>) {
  45. if (event.key === 'Tab' || event.key === 'Escape') {
  46. event.preventDefault()
  47. close()
  48. }
  49. }
  50. const handleKeyDown = useCallback(() => {
  51. keyboardInputRef.current = true
  52. }, [])
  53. const handleMouseDown = useCallback(() => {
  54. keyboardInputRef.current = false
  55. }, [])
  56. const handleShiftContextMenu = useCallback(
  57. (event: MouseEvent) => {
  58. if (event.shiftKey) {
  59. setContextMenuCoords(null)
  60. }
  61. },
  62. [setContextMenuCoords]
  63. )
  64. useEffect(() => {
  65. document.addEventListener('keydown', handleKeyDown)
  66. document.addEventListener('mousedown', handleMouseDown)
  67. document.addEventListener('contextmenu', handleShiftContextMenu)
  68. return () => {
  69. document.removeEventListener('keydown', handleKeyDown)
  70. document.removeEventListener('mousedown', handleMouseDown)
  71. document.removeEventListener('contextmenu', handleShiftContextMenu)
  72. }
  73. }, [handleKeyDown, handleMouseDown, handleShiftContextMenu])
  74. if (!contextMenuCoords || fileTreeReadOnly) return null
  75. const dropDirection =
  76. document.body.offsetHeight / contextMenuCoords.top < 2 &&
  77. document.body.offsetHeight - contextMenuCoords.top < 250
  78. ? 'up'
  79. : 'down'
  80. return ReactDOM.createPortal(
  81. <div
  82. style={contextMenuCoords}
  83. // TODO ide-redesign-cleanup: remove 'ide-redesign-main' class when old editor is removed
  84. // It is only used to apply dark theme styles to the context menu in the new editor
  85. className="context-menu ide-redesign-main"
  86. >
  87. <Dropdown
  88. show
  89. drop={dropDirection}
  90. onKeyDown={handleClose}
  91. onToggle={handleToggle}
  92. >
  93. <DropdownMenu
  94. className={classNames('dropdown-menu-sm-width', {
  95. // We have to manually add a class to handle upwards context menu styling
  96. // due to the way that this dropdown is positioned with absolute coordinates and
  97. // not relative to a toggle
  98. 'context-menu-upwards': dropDirection === 'up',
  99. })}
  100. id="dropdown-file-tree-context-menu"
  101. >
  102. <FileTreeItemMenuItems />
  103. </DropdownMenu>
  104. </Dropdown>
  105. </div>,
  106. document.body
  107. )
  108. }
  109. export default FileTreeContextMenu