tabs-context-menu.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import { useEffect, useRef } from 'react'
  2. import ReactDOM from 'react-dom'
  3. import { useTranslation } from 'react-i18next'
  4. import {
  5. Dropdown,
  6. DropdownItem,
  7. DropdownMenu,
  8. } from '@/shared/components/dropdown/dropdown-menu'
  9. import { useTabsContext } from '@/features/ide-react/context/tabs-context'
  10. export function TabsContextMenu() {
  11. const { t } = useTranslation()
  12. const {
  13. tabs,
  14. closeTab,
  15. closeOtherTabs,
  16. contextMenuTarget,
  17. setContextMenuTarget,
  18. } = useTabsContext()
  19. const menuRef = useRef<HTMLDivElement>(null)
  20. // Close the context menu when opening another one
  21. useEffect(() => {
  22. if (!contextMenuTarget) return
  23. const handler = (event: MouseEvent) => {
  24. if (event.button !== 2) return
  25. const target = event.target as Element | null
  26. // Right-clicking another tab should re-open the menu on that tab;
  27. // its onContextMenu handler will set the new target.
  28. if (target?.closest('.context-menu')) return
  29. if (target?.closest('.editor-file-tab')) return
  30. setContextMenuTarget(null)
  31. }
  32. // Listen on mousedown (not contextmenu) so we still close when another
  33. // component's handler calls preventDefault on the contextmenu event.
  34. document.addEventListener('mousedown', handler)
  35. return () => document.removeEventListener('mousedown', handler)
  36. }, [contextMenuTarget, setContextMenuTarget])
  37. // Move focus to the menu on open, and back to the originating tab on close.
  38. const lastTabIdRef = useRef<string | null>(null)
  39. useEffect(() => {
  40. if (contextMenuTarget) {
  41. lastTabIdRef.current = contextMenuTarget.tabId
  42. menuRef.current?.querySelector<HTMLElement>('[role="menu"]')?.focus()
  43. } else if (lastTabIdRef.current) {
  44. document
  45. .querySelector<HTMLElement>(`[data-tab-id="${lastTabIdRef.current}"]`)
  46. ?.focus()
  47. lastTabIdRef.current = null
  48. }
  49. }, [contextMenuTarget])
  50. if (!contextMenuTarget) return null
  51. const close = () => setContextMenuTarget(null)
  52. const handleKeyDown = (event: React.KeyboardEvent<Element>) => {
  53. if (event.key === 'Tab' || event.key === 'Escape') {
  54. event.preventDefault()
  55. close()
  56. }
  57. }
  58. const handleToggle = (wantOpen: boolean) => {
  59. if (!wantOpen) close()
  60. }
  61. return ReactDOM.createPortal(
  62. <div
  63. ref={menuRef}
  64. style={{ top: contextMenuTarget.top, left: contextMenuTarget.left }}
  65. // TODO ide-redesign-cleanup: remove 'ide-redesign-main' class when old editor is removed
  66. className="context-menu ide-redesign-main"
  67. >
  68. <Dropdown
  69. show
  70. drop="down"
  71. onKeyDown={handleKeyDown}
  72. onToggle={handleToggle}
  73. >
  74. <DropdownMenu className="dropdown-menu-sm-width" tabIndex={-1}>
  75. <DropdownItem
  76. disabled={tabs.length <= 1}
  77. as="button"
  78. onClick={() => {
  79. closeTab(contextMenuTarget.tabId)
  80. close()
  81. }}
  82. >
  83. {t('close_tab')}
  84. </DropdownItem>
  85. <DropdownItem
  86. disabled={tabs.length <= 1}
  87. as="button"
  88. onClick={() => {
  89. closeOtherTabs(contextMenuTarget.tabId)
  90. close()
  91. }}
  92. >
  93. {t('close_others')}
  94. </DropdownItem>
  95. </DropdownMenu>
  96. </Dropdown>
  97. </div>,
  98. document.body
  99. )
  100. }