file-tree-context-menu.tsx 2.9 KB

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