file-tree-context-menu.tsx 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import React, { useEffect, useRef } from 'react'
  2. import ReactDOM from 'react-dom'
  3. import { Dropdown } from 'react-bootstrap'
  4. import { useFileTreeData } from '@/shared/context/file-tree-data-context'
  5. import { useFileTreeMainContext } from '../contexts/file-tree-main'
  6. import FileTreeItemMenuItems from './file-tree-item/file-tree-item-menu-items'
  7. function FileTreeContextMenu() {
  8. const { fileTreeReadOnly } = useFileTreeData()
  9. const { contextMenuCoords, setContextMenuCoords } = useFileTreeMainContext()
  10. const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
  11. useEffect(() => {
  12. if (contextMenuCoords) {
  13. toggleButtonRef.current = document.querySelector(
  14. '.entity-menu-toggle'
  15. ) as HTMLButtonElement | null
  16. focusContextMenu()
  17. }
  18. }, [contextMenuCoords])
  19. if (!contextMenuCoords || fileTreeReadOnly) return null
  20. // A11y - Move the focus to the context menu when it opens
  21. function focusContextMenu() {
  22. const contextMenu = document.querySelector(
  23. '[aria-labelledby="dropdown-file-tree-context-menu"]'
  24. ) as HTMLElement | null
  25. contextMenu?.focus()
  26. }
  27. function close() {
  28. setContextMenuCoords(null)
  29. if (toggleButtonRef.current) {
  30. // A11y - Move the focus back to the toggle button when the context menu closes by pressing the Esc key
  31. toggleButtonRef.current.focus()
  32. }
  33. }
  34. function handleToggle(wantOpen: boolean) {
  35. if (!wantOpen) close()
  36. }
  37. function handleClick() {
  38. handleToggle(false)
  39. }
  40. // A11y - Close the context menu when the user presses the Tab key
  41. // Focus should move to the next element in the filetree
  42. function handleKeyDown(event: React.KeyboardEvent<Dropdown>) {
  43. if (event.key === 'Tab') {
  44. close()
  45. }
  46. }
  47. return ReactDOM.createPortal(
  48. <Dropdown
  49. onClick={handleClick}
  50. open
  51. id="dropdown-file-tree-context-menu"
  52. onToggle={handleToggle}
  53. dropup={
  54. document.body.offsetHeight / contextMenuCoords.top < 2 &&
  55. document.body.offsetHeight - contextMenuCoords.top < 250
  56. }
  57. className="context-menu"
  58. style={contextMenuCoords}
  59. onKeyDown={handleKeyDown}
  60. >
  61. <FakeDropDownToggle bsRole="toggle" />
  62. <Dropdown.Menu tabIndex={-1}>
  63. <FileTreeItemMenuItems />
  64. </Dropdown.Menu>
  65. </Dropdown>,
  66. document.body
  67. )
  68. }
  69. // fake component required as Dropdowns require a Toggle, even tho we don't want
  70. // one for the context menu
  71. const FakeDropDownToggle = React.forwardRef<undefined, { bsRole: string }>(
  72. ({ bsRole }, ref) => {
  73. return null
  74. }
  75. )
  76. FakeDropDownToggle.displayName = 'FakeDropDownToggle'
  77. export default FileTreeContextMenu