feat: implement Cmd+Enter as commit shortcut in Source Control (#9773)
* feat: implement Cmd+Enter as commit shortcut in Source Control * test: add unit tests for commit shortcut and tooltip formatting * fix: address review feedback on modifier keys and test coverage * test: split mac and windows/linux shortcut and keydown tests --------- Co-authored-by: Andres Van Reepingen <andres.vanreepingen@datacamp.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
This commit is contained in:
parent
6577b79e2e
commit
18295c27e8
|
|
@ -1,6 +1,14 @@
|
|||
import { describe, expect, it, vi } from 'vitest'
|
||||
// @vitest-environment happy-dom
|
||||
import React from 'react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { CommitArea, ConflictSummaryCard, OperationBanner } from './SourceControl'
|
||||
import { fireEvent, render } from '@testing-library/react'
|
||||
import {
|
||||
CommitArea,
|
||||
ConflictSummaryCard,
|
||||
handleSourceControlCommitShortcut,
|
||||
OperationBanner
|
||||
} from './SourceControl'
|
||||
import {
|
||||
resolveCommitAreaPrimaryAction,
|
||||
type PrimaryActionInputs
|
||||
|
|
@ -9,6 +17,23 @@ import { resolveDropdownItems, type DropdownActionKind } from './source-control-
|
|||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { deriveSourceControlPushRecovery } from './source-control-push-recovery'
|
||||
|
||||
vi.mock('@/components/ui/tooltip', () => ({
|
||||
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
TooltipTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
TooltipContent: ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
||||
<div className={className}>{children}</div>
|
||||
),
|
||||
TooltipProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
function setUserAgent(userAgent: string): void {
|
||||
vi.stubGlobal('navigator', { userAgent })
|
||||
}
|
||||
|
||||
function buildInputs(overrides: Partial<PrimaryActionInputs> = {}): PrimaryActionInputs {
|
||||
return {
|
||||
stagedCount: 1,
|
||||
|
|
@ -146,6 +171,102 @@ describe('CommitArea', () => {
|
|||
expect(hasDisabledAttribute(firstButton(renderCommitArea(baseProps())))).toBe(false)
|
||||
})
|
||||
|
||||
it('renders the Commit shortcut key indicator (⌘Enter) in primary button tooltip on macOS', () => {
|
||||
const props = baseProps()
|
||||
setUserAgent('Macintosh')
|
||||
const markupMac = renderCommitArea({
|
||||
...props,
|
||||
primaryAction: { kind: 'commit', disabled: false, label: 'Commit', title: 'Commit changes' }
|
||||
})
|
||||
expect(markupMac).toContain('Commit changes')
|
||||
expect(markupMac).toContain('⌘')
|
||||
expect(markupMac).toContain('Enter')
|
||||
})
|
||||
|
||||
it('renders the Commit shortcut key indicator (Ctrl+Enter) in primary button tooltip on Windows/Linux', () => {
|
||||
const props = baseProps()
|
||||
setUserAgent('Windows NT')
|
||||
const markupWin = renderCommitArea({
|
||||
...props,
|
||||
primaryAction: { kind: 'commit', disabled: false, label: 'Commit', title: 'Commit changes' }
|
||||
})
|
||||
expect(markupWin).toContain('Commit changes')
|
||||
expect(markupWin).toContain('Ctrl')
|
||||
expect(markupWin).toContain('+')
|
||||
expect(markupWin).toContain('Enter')
|
||||
})
|
||||
|
||||
it('only handles Cmd+Enter when focus is within the Source Control sidebar', () => {
|
||||
setUserAgent('Macintosh')
|
||||
const onPrimaryAction = vi.fn()
|
||||
const primaryAction = {
|
||||
kind: 'commit' as const,
|
||||
disabled: false
|
||||
}
|
||||
const { getByTestId } = render(
|
||||
<>
|
||||
<div
|
||||
data-testid="source-control-sidebar"
|
||||
onKeyDown={(event) =>
|
||||
handleSourceControlCommitShortcut(event, primaryAction, onPrimaryAction)
|
||||
}
|
||||
>
|
||||
<button type="button" data-testid="inside-sidebar">
|
||||
Inside
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" data-testid="outside-sidebar">
|
||||
Outside
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
|
||||
const outside = getByTestId('outside-sidebar')
|
||||
outside.focus()
|
||||
fireEvent.keyDown(outside, { key: 'Enter', metaKey: true })
|
||||
expect(onPrimaryAction).not.toHaveBeenCalled()
|
||||
|
||||
const inside = getByTestId('inside-sidebar')
|
||||
inside.focus()
|
||||
fireEvent.keyDown(inside, { key: 'Enter', metaKey: true })
|
||||
expect(onPrimaryAction).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('handles Ctrl+Enter, but not Cmd+Enter, inside the sidebar on Windows/Linux', () => {
|
||||
setUserAgent('Linux')
|
||||
const onPrimaryAction = vi.fn()
|
||||
const primaryAction = {
|
||||
kind: 'commit' as const,
|
||||
disabled: false
|
||||
}
|
||||
const { getByRole } = render(
|
||||
<button
|
||||
type="button"
|
||||
onKeyDown={(event) =>
|
||||
handleSourceControlCommitShortcut(event, primaryAction, onPrimaryAction)
|
||||
}
|
||||
>
|
||||
Commit scope
|
||||
</button>
|
||||
)
|
||||
const target = getByRole('button', { name: 'Commit scope' })
|
||||
|
||||
fireEvent.keyDown(target, { key: 'Enter', metaKey: true })
|
||||
expect(onPrimaryAction).not.toHaveBeenCalled()
|
||||
|
||||
fireEvent.keyDown(target, { key: 'Enter', ctrlKey: true })
|
||||
expect(onPrimaryAction).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('does not render the Commit shortcut keys inside the primary button tooltip when the action is not commit', () => {
|
||||
const props = baseProps()
|
||||
const markup = renderCommitArea({
|
||||
...props,
|
||||
primaryAction: { kind: 'push', disabled: false, label: 'Push', title: 'Push changes' }
|
||||
})
|
||||
expect(markup).not.toContain('Enter')
|
||||
})
|
||||
|
||||
it('disables the textarea while the commit is in flight', () => {
|
||||
const markup = renderCommitArea({
|
||||
...baseProps({ isCommitting: true }),
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ import { WORKSPACE_FILE_PATH_MIME } from '@/lib/workspace-file-drag'
|
|||
import { isFolderRepo } from '../../../../shared/repo-kind'
|
||||
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo'
|
||||
import { getScreenSubmitModifierLabel, isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -4708,6 +4710,13 @@ function SourceControlInner(): React.JSX.Element {
|
|||
runCreatePrIntent
|
||||
])
|
||||
|
||||
const handleSourceControlKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
handleSourceControlCommitShortcut(event, primaryAction, handlePrimaryClick)
|
||||
},
|
||||
[handlePrimaryClick, primaryAction]
|
||||
)
|
||||
|
||||
const handleCreatePrHeaderClick = useCallback((): void => {
|
||||
if (!createPrHeaderAction || createPrHeaderAction.disabled) {
|
||||
return
|
||||
|
|
@ -5448,7 +5457,11 @@ function SourceControlInner(): React.JSX.Element {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div ref={setSourceControlRoot} className="relative flex h-full flex-col overflow-hidden">
|
||||
<div
|
||||
ref={setSourceControlRoot}
|
||||
className="relative flex h-full flex-col overflow-hidden"
|
||||
onKeyDown={handleSourceControlKeyDown}
|
||||
>
|
||||
<SourceControlHeaderToolbar
|
||||
filterQuery={filterQuery}
|
||||
filterExpanded={filterExpanded}
|
||||
|
|
@ -6358,6 +6371,20 @@ function SourceControlInner(): React.JSX.Element {
|
|||
const SourceControl = React.memo(SourceControlInner)
|
||||
export default SourceControl
|
||||
|
||||
export function handleSourceControlCommitShortcut(
|
||||
event: React.KeyboardEvent<HTMLElement>,
|
||||
primaryAction: Pick<PrimaryAction, 'disabled' | 'kind'>,
|
||||
onCommit: () => void
|
||||
): void {
|
||||
if (primaryAction.disabled || primaryAction.kind !== 'commit' || !isScreenSubmitShortcut(event)) {
|
||||
return
|
||||
}
|
||||
// Why: the handler lives on the Source Control root, so the shortcut cannot fire from the editor, terminal, or another sidebar tab.
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
onCommit()
|
||||
}
|
||||
|
||||
type CommitAreaProps = {
|
||||
worktreeId: string | null
|
||||
groupId: string | null
|
||||
|
|
@ -6690,8 +6717,11 @@ export function CommitArea({
|
|||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={6} className="max-w-72">
|
||||
{primaryAction.title}
|
||||
<TooltipContent side="top" sideOffset={6} className="flex max-w-72 items-center gap-2">
|
||||
<span>{primaryAction.title}</span>
|
||||
{primaryAction.kind === 'commit' ? (
|
||||
<ShortcutKeyCombo keys={[getScreenSubmitModifierLabel(), 'Enter']} />
|
||||
) : null}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenu>
|
||||
|
|
|
|||
Loading…
Reference in New Issue