file-tree-context-menu.tsx 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import React, { useCallback, useEffect, useRef } from 'react'
  2. import ReactDOM from 'react-dom'
  3. import {
  4. Dropdown,
  5. DropdownMenu,
  6. } from '@/features/ui/components/bootstrap-5/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. function FileTreeContextMenu() {
  11. const { fileTreeReadOnly } = useFileTreeData()
  12. const { contextMenuCoords, setContextMenuCoords } = useFileTreeMainContext()
  13. const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
  14. const keyboardInputRef = useRef(false)
  15. useEffect(() => {
  16. if (contextMenuCoords) {
  17. toggleButtonRef.current = document.querySelector(
  18. '.entity-menu-toggle'
  19. ) as HTMLButtonElement | null
  20. }
  21. }, [contextMenuCoords])
  22. useEffect(() => {
  23. if (contextMenuCoords && keyboardInputRef.current) {
  24. const firstDropdownMenuItem = document.querySelector(
  25. '#dropdown-file-tree-context-menu .dropdown-item:not([disabled])'
  26. ) as HTMLButtonElement | null
  27. if (firstDropdownMenuItem) {
  28. firstDropdownMenuItem.focus()
  29. }
  30. }
  31. }, [contextMenuCoords])
  32. function close() {
  33. if (!contextMenuCoords) return
  34. setContextMenuCoords(null)
  35. if (toggleButtonRef.current) {
  36. // A11y - Focus moves back to the trigger button when the context menu is dismissed
  37. toggleButtonRef.current.focus()
  38. }
  39. }
  40. function handleToggle(wantOpen: boolean) {
  41. if (!wantOpen) close()
  42. }
  43. function handleClose(event: React.KeyboardEvent<Element>) {
  44. if (event.key === 'Tab' || event.key === 'Escape') {
  45. event.preventDefault()
  46. close()
  47. }
  48. }
  49. const handleKeyDown = useCallback(() => {
  50. keyboardInputRef.current = true
  51. }, [])
  52. const handleMouseDown = useCallback(() => {
  53. keyboardInputRef.current = false
  54. }, [])
  55. useEffect(() => {
  56. document.addEventListener('keydown', handleKeyDown)
  57. document.addEventListener('mousedown', handleMouseDown)
  58. return () => {
  59. document.removeEventListener('keydown', handleKeyDown)
  60. document.removeEventListener('mousedown', handleMouseDown)
  61. }
  62. }, [handleKeyDown, handleMouseDown])
  63. if (!contextMenuCoords || fileTreeReadOnly) return null
  64. return ReactDOM.createPortal(
  65. <div style={contextMenuCoords} className="context-menu">
  66. <Dropdown
  67. show
  68. drop={
  69. document.body.offsetHeight / contextMenuCoords.top < 2 &&
  70. document.body.offsetHeight - contextMenuCoords.top < 250
  71. ? 'up'
  72. : 'down'
  73. }
  74. onKeyDown={handleClose}
  75. onToggle={handleToggle}
  76. >
  77. <DropdownMenu
  78. className="dropdown-menu-sm-width"
  79. id="dropdown-file-tree-context-menu"
  80. >
  81. <FileTreeItemMenuItems />
  82. </DropdownMenu>
  83. </Dropdown>
  84. </div>,
  85. document.body
  86. )
  87. }
  88. export default FileTreeContextMenu