file-tree-context-menu.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  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. useEffect(() => {
  57. document.addEventListener('keydown', handleKeyDown)
  58. document.addEventListener('mousedown', handleMouseDown)
  59. return () => {
  60. document.removeEventListener('keydown', handleKeyDown)
  61. document.removeEventListener('mousedown', handleMouseDown)
  62. }
  63. }, [handleKeyDown, handleMouseDown])
  64. if (!contextMenuCoords || fileTreeReadOnly) return null
  65. const dropDirection =
  66. document.body.offsetHeight / contextMenuCoords.top < 2 &&
  67. document.body.offsetHeight - contextMenuCoords.top < 250
  68. ? 'up'
  69. : 'down'
  70. return ReactDOM.createPortal(
  71. <div
  72. style={contextMenuCoords}
  73. // TODO ide-redesign-cleanup: remove 'ide-redesign-main' class when old editor is removed
  74. // It is only used to apply dark theme styles to the context menu in the new editor
  75. className="context-menu ide-redesign-main"
  76. >
  77. <Dropdown
  78. show
  79. drop={dropDirection}
  80. onKeyDown={handleClose}
  81. onToggle={handleToggle}
  82. >
  83. <DropdownMenu
  84. className={classNames('dropdown-menu-sm-width', {
  85. // We have to manually add a class to handle upwards context menu styling
  86. // due to the way that this dropdown is positioned with absolute coordinates and
  87. // not relative to a toggle
  88. 'context-menu-upwards': dropDirection === 'up',
  89. })}
  90. id="dropdown-file-tree-context-menu"
  91. >
  92. <FileTreeItemMenuItems />
  93. </DropdownMenu>
  94. </Dropdown>
  95. </div>,
  96. document.body
  97. )
  98. }
  99. export default FileTreeContextMenu