file-tree-context-menu.tsx 2.8 KB

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