file-tree-context-menu.tsx 3.6 KB

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