mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 16:02:43 +00:00
fix: open editor rename after context menu releases focus (#18934)
* fix: open editor rename after context menu releases focus * refactor(editor): tighten rename focus-handoff comments and test setup Correct the rename-input focus comment that still credited the animation frame with outrunning menu teardown, clarify why the rename now runs from onCloseAutoFocus, and fold the repeated menu-close invocation in the tab tests into one helper.
This commit is contained in:
@@ -345,6 +345,15 @@ function findMenuItemByText(node: unknown, label: string): ReactElementLike {
|
||||
return item
|
||||
}
|
||||
|
||||
/** Picks Rename, then fires the close-autofocus that actually opens the input. */
|
||||
function selectRenameFromMenu(node: unknown): void {
|
||||
;(findMenuItemByText(node, 'Rename').props.onSelect as () => void)()
|
||||
const content = findElementsByType(node, 'DropdownMenuContent')[0]!
|
||||
;(content.props.onCloseAutoFocus as (event: { preventDefault: () => void }) => void)({
|
||||
preventDefault: vi.fn()
|
||||
})
|
||||
}
|
||||
|
||||
function findSpanByText(node: unknown, label: string): ReactElementLike {
|
||||
const span = findElementsByType(node, 'span').find(
|
||||
(candidate) =>
|
||||
@@ -401,7 +410,7 @@ describe('EditorFileTab rename menu', () => {
|
||||
// isUntitled; the tab menu must let users rename the screenshot-style
|
||||
// "untitled-N.md" files directly.
|
||||
expect(renameItem.props.disabled).toBe(false)
|
||||
;(renameItem.props.onSelect as () => void)()
|
||||
selectRenameFromMenu(firstRender)
|
||||
|
||||
const secondRender = expandNode((await renderEditorFileTab(file, onActivate)).element)
|
||||
const inputs = findElementsByType(secondRender, 'input')
|
||||
@@ -425,9 +434,8 @@ describe('EditorFileTab rename menu', () => {
|
||||
it('ignores IME composition Enter before renaming the editor file tab', async () => {
|
||||
const file = baseFile()
|
||||
const firstRender = expandNode((await renderEditorFileTab(file)).element)
|
||||
const renameItem = findMenuItemByText(firstRender, 'Rename')
|
||||
|
||||
;(renameItem.props.onSelect as () => void)()
|
||||
selectRenameFromMenu(firstRender)
|
||||
|
||||
const secondRender = expandNode((await renderEditorFileTab(file)).element)
|
||||
const input = findElementsByType(secondRender, 'input')[0]
|
||||
@@ -457,9 +465,8 @@ describe('EditorFileTab rename menu', () => {
|
||||
it('does not re-commit when unmounting the rename input emits multiple blur events', async () => {
|
||||
const file = baseFile()
|
||||
const firstRender = expandNode((await renderEditorFileTab(file)).element)
|
||||
const renameItem = findMenuItemByText(firstRender, 'Rename')
|
||||
|
||||
;(renameItem.props.onSelect as () => void)()
|
||||
selectRenameFromMenu(firstRender)
|
||||
|
||||
const secondRender = expandNode((await renderEditorFileTab(file)).element)
|
||||
const input = findElementsByType(secondRender, 'input')[0]
|
||||
|
||||
@@ -171,8 +171,8 @@ export default function EditorFileTab({
|
||||
if (!input) {
|
||||
return
|
||||
}
|
||||
// Why: Radix closes the context menu after onSelect; defer focus so its
|
||||
// teardown cannot steal focus back or blur-commit the newly mounted input.
|
||||
// Why: the tab re-lays out around the input; focus on the next frame so
|
||||
// that swap has settled before selecting text.
|
||||
renameFocusFrameRef.current = requestAnimationFrame(() => {
|
||||
renameFocusFrameRef.current = null
|
||||
if (renameInputRef.current !== input) {
|
||||
|
||||
@@ -202,7 +202,9 @@ function extractText(node: unknown): string {
|
||||
return el.props && 'children' in el.props ? extractText(el.props.children) : ''
|
||||
}
|
||||
|
||||
async function renderMenu(): Promise<unknown> {
|
||||
async function renderMenu(
|
||||
overrides: { onActivate?: () => void; onOpenRenameInput?: () => void } = {}
|
||||
): Promise<unknown> {
|
||||
const module = await import('./EditorFileTabContextMenu')
|
||||
return module.EditorFileTabContextMenu({
|
||||
open: true,
|
||||
@@ -238,7 +240,8 @@ async function renderMenu(): Promise<unknown> {
|
||||
onCloseAll: vi.fn(),
|
||||
onCloseToRight: vi.fn(),
|
||||
onCloseToLeft: vi.fn(),
|
||||
onOpenMarkdownPreview: vi.fn()
|
||||
onOpenMarkdownPreview: vi.fn(),
|
||||
...overrides
|
||||
})
|
||||
}
|
||||
|
||||
@@ -266,6 +269,27 @@ describe('EditorFileTabContextMenu close-all shortcut', () => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('opens rename only after menu close releases focus and consumes the request once', async () => {
|
||||
const onActivate = vi.fn()
|
||||
const onOpenRenameInput = vi.fn()
|
||||
const tree = expandNode(await renderMenu({ onActivate, onOpenRenameInput }))
|
||||
const rename = findElementsByType(tree, 'DropdownMenuItem').find((item) =>
|
||||
extractText(item.props.children).includes('Rename')
|
||||
)!
|
||||
const content = findElementsByType(tree, 'DropdownMenuContent')[0]!
|
||||
;(rename.props.onSelect as () => void)()
|
||||
expect(onActivate).not.toHaveBeenCalled()
|
||||
expect(onOpenRenameInput).not.toHaveBeenCalled()
|
||||
const preventDefault = vi.fn()
|
||||
const close = content.props.onCloseAutoFocus as (event: { preventDefault: () => void }) => void
|
||||
close({ preventDefault })
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(onActivate).toHaveBeenCalledTimes(1)
|
||||
expect(onOpenRenameInput).toHaveBeenCalledTimes(1)
|
||||
close({ preventDefault })
|
||||
expect(onOpenRenameInput).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('renders assigned shortcuts next to Rename, Close, and Close All Editor Tabs', async () => {
|
||||
const tree = expandNode(await renderMenu())
|
||||
const menuItems = findElementsByType(tree, 'DropdownMenuItem')
|
||||
|
||||
@@ -126,6 +126,10 @@ export function EditorFileTabContextMenu({
|
||||
}
|
||||
skipMenuFocusRestoreRef.current = false
|
||||
event.preventDefault()
|
||||
// Why: opening the input in onSelect lets the still-closing menu reclaim
|
||||
// focus, and the resulting blur commits the rename away before the user types.
|
||||
onActivate()
|
||||
onOpenRenameInput()
|
||||
}}
|
||||
>
|
||||
<TabWorkspaceLayoutMenuSection
|
||||
@@ -137,8 +141,6 @@ export function EditorFileTabContextMenu({
|
||||
disabled={!canRename || isRenaming}
|
||||
onSelect={() => {
|
||||
skipMenuFocusRestoreRef.current = true
|
||||
onActivate()
|
||||
onOpenRenameInput()
|
||||
}}
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
|
||||
Reference in New Issue
Block a user