file-tree-context-menu.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  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. import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
  12. function FileTreeContextMenu() {
  13. const { fileTreeReadOnly } = useFileTreeData()
  14. const { contextMenuCoords, setContextMenuCoords } = useFileTreeMainContext()
  15. const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
  16. useEffect(() => {
  17. if (contextMenuCoords) {
  18. toggleButtonRef.current = document.querySelector(
  19. '.entity-menu-toggle'
  20. ) as HTMLButtonElement | null
  21. focusContextMenu()
  22. }
  23. }, [contextMenuCoords])
  24. if (!contextMenuCoords || fileTreeReadOnly) return null
  25. // A11y - Move the focus to the context menu when it opens
  26. function focusContextMenu() {
  27. const BS3contextMenu = document.querySelector(
  28. '[aria-labelledby="dropdown-file-tree-context-menu"]'
  29. ) as HTMLElement | null
  30. BS3contextMenu?.focus()
  31. }
  32. function close() {
  33. setContextMenuCoords(null)
  34. if (toggleButtonRef.current) {
  35. // A11y - Move the focus back to the toggle button when the context menu closes by pressing the Esc key
  36. toggleButtonRef.current.focus()
  37. }
  38. }
  39. function handleToggle(wantOpen: boolean) {
  40. if (!wantOpen) close()
  41. }
  42. function handleClick() {
  43. handleToggle(false)
  44. }
  45. // A11y - Close the context menu when the user presses the Tab key
  46. // Focus should move to the next element in the filetree
  47. function handleKeyDown(event: React.KeyboardEvent<BS3Dropdown | Element>) {
  48. if (event.key === 'Tab') {
  49. close()
  50. }
  51. }
  52. return ReactDOM.createPortal(
  53. <BootstrapVersionSwitcher
  54. bs3={
  55. <BS3Dropdown
  56. onClick={handleClick}
  57. open
  58. id="dropdown-file-tree-context-menu"
  59. onToggle={handleToggle}
  60. dropup={
  61. document.body.offsetHeight / contextMenuCoords.top < 2 &&
  62. document.body.offsetHeight - contextMenuCoords.top < 250
  63. }
  64. className="context-menu"
  65. style={contextMenuCoords}
  66. onKeyDown={handleKeyDown}
  67. >
  68. <FakeDropDownToggle bsRole="toggle" />
  69. <BS3Dropdown.Menu tabIndex={-1}>
  70. <FileTreeItemMenuItems />
  71. </BS3Dropdown.Menu>
  72. </BS3Dropdown>
  73. }
  74. bs5={
  75. <div style={contextMenuCoords} className="context-menu">
  76. <Dropdown
  77. show
  78. drop={
  79. document.body.offsetHeight / contextMenuCoords.top < 2 &&
  80. document.body.offsetHeight - contextMenuCoords.top < 250
  81. ? 'up'
  82. : 'down'
  83. }
  84. focusFirstItemOnShow // A11y - Focus the first item in the context menu when it opens since the menu is rendered at the root level
  85. onKeyDown={handleKeyDown}
  86. onToggle={handleToggle}
  87. >
  88. <DropdownMenu
  89. className="dropdown-menu-sm-width"
  90. id="dropdown-file-tree-context-menu"
  91. >
  92. <FileTreeItemMenuItems />
  93. </DropdownMenu>
  94. </Dropdown>
  95. </div>
  96. }
  97. />,
  98. document.body
  99. )
  100. }
  101. // fake component required as Dropdowns require a Toggle, even tho we don't want
  102. // one for the context menu
  103. const FakeDropDownToggle = React.forwardRef<undefined, { bsRole: string }>(
  104. ({ bsRole }, ref) => {
  105. return null
  106. }
  107. )
  108. FakeDropDownToggle.displayName = 'FakeDropDownToggle'
  109. export default FileTreeContextMenu