tabs-context-menu.tsx 4.2 KB

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