diff --git a/src/renderer/src/components/new-workspace/RunTargetComboboxRow.tsx b/src/renderer/src/components/new-workspace/RunTargetComboboxRow.tsx index e02e34ce3b4..dde3ae66b28 100644 --- a/src/renderer/src/components/new-workspace/RunTargetComboboxRow.tsx +++ b/src/renderer/src/components/new-workspace/RunTargetComboboxRow.tsx @@ -6,8 +6,6 @@ import { cn } from '@/lib/utils' import type { ExecutionHostId } from '../../../../shared/execution-host' import { ProjectOptionDetail } from './ProjectComboboxRow' import { translate } from '@/i18n/i18n' -// Re-exported from its shared home so both the composer and the sidebar draw -// one host glyph vocabulary. import { HostRowIcon } from '../host-row-icon' export { HostRowIcon } diff --git a/src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.tsx b/src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.tsx index 96967e8a5f0..9418b29b5ce 100644 --- a/src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.tsx +++ b/src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.tsx @@ -159,7 +159,11 @@ export default function ImportedWorktreesVisibilityLine({ {hostContextLabel && placement !== 'pinned-fallback' ? ( {hostContextHostId ? ( - + ) : null} {hostContextLabel} diff --git a/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.tsx b/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.tsx index 856ebe8bf11..33fe674575f 100644 --- a/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.tsx +++ b/src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.tsx @@ -103,7 +103,11 @@ export default function NewExternalWorktreesInboxLine({ {hostContextLabel ? ( {hostContextHostId ? ( - + ) : null} {hostContextLabel} diff --git a/src/renderer/src/components/sidebar/NoticeHostGlyph.test.tsx b/src/renderer/src/components/sidebar/NoticeHostGlyph.test.tsx index a6b642aca89..46bf7cd371f 100644 --- a/src/renderer/src/components/sidebar/NoticeHostGlyph.test.tsx +++ b/src/renderer/src/components/sidebar/NoticeHostGlyph.test.tsx @@ -1,16 +1,19 @@ // @vitest-environment happy-dom /** - * Notice rows reuse the worktree card's host indicator so one project's rows - * read the same way wherever they appear: a server glyph with a "Project on …" - * tooltip, nothing at all for local. + * Notice rows reuse one host vocabulary: monitor for local, server for remote, + * with the worktree card's "Project on …" tooltip copy. */ -import { act, type ReactNode } from 'react' +import { act, cloneElement, type ReactElement, type ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import NoticeHostGlyph from './NoticeHostGlyph' import en from '../../i18n/locales/en.json' +import es from '../../i18n/locales/es.json' +import ja from '../../i18n/locales/ja.json' +import ko from '../../i18n/locales/ko.json' +import zh from '../../i18n/locales/zh.json' const runtimeStatusByEnvironmentId = new Map() @@ -20,7 +23,8 @@ vi.mock('@/store', () => ({ vi.mock('@/components/ui/tooltip', () => ({ Tooltip: ({ children }: { children: ReactNode }) => <>{children}, - TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}, + TooltipTrigger: ({ children }: { children: ReactElement<{ 'data-testid'?: string }> }) => + cloneElement(children, { 'data-testid': 'tooltip-trigger' }), TooltipContent: ({ children }: { children: ReactNode }) => ( {children} ) @@ -28,13 +32,23 @@ vi.mock('@/components/ui/tooltip', () => ({ const roots: Root[] = [] -async function render(hostId: string, hostLabel = 'openclaw'): Promise { +async function render( + hostId: string, + hostLabel = 'openclaw', + keyboardFocusable = false +): Promise { const container = document.createElement('div') document.body.appendChild(container) const root = createRoot(container) roots.push(root) await act(async () => { - root.render() + root.render( + + ) }) return container } @@ -86,6 +100,23 @@ describe('NoticeHostGlyph', () => { ) }) + it('makes a passive row glyph keyboard reachable with an accessible name', async () => { + const container = await render('ssh:openclaw-target', 'openclaw', true) + const trigger = container.querySelector('[data-testid="tooltip-trigger"]') + + expect(trigger?.getAttribute('tabindex')).toBe('0') + expect(trigger?.getAttribute('role')).toBe('img') + expect(trigger?.getAttribute('aria-label')).toBe('Project on SSH host openclaw') + }) + + it('does not add a nested tab stop when the glyph is inside a button', async () => { + const container = await render('ssh:openclaw-target') + + expect( + container.querySelector('[data-testid="tooltip-trigger"]')?.hasAttribute('tabindex') + ).toBe(false) + }) + it('draws one glyph vocabulary: a monitor for local, a server for remote', async () => { const local = await render('local', 'Local Mac') const remote = await render('ssh:openclaw-target') @@ -111,4 +142,16 @@ describe('NoticeHostGlyph', () => { runtimeHostProject: 'Project on {{hostName}}' }) }) + + it.each(Object.entries({ es, ja, ko, zh }))( + 'keeps its copy in the %s catalog', + (_locale, catalog) => { + expect(catalog.auto.components.sidebar.NoticeHostGlyph).toMatchObject({ + hostDisconnected: expect.stringContaining('{{hostName}}'), + sshHostProject: expect.stringContaining('{{hostName}}'), + localHostProject: expect.any(String), + runtimeHostProject: expect.stringContaining('{{hostName}}') + }) + } + ) }) diff --git a/src/renderer/src/components/sidebar/NoticeHostGlyph.tsx b/src/renderer/src/components/sidebar/NoticeHostGlyph.tsx index 9dfd0c8cbb7..db1616203dd 100644 --- a/src/renderer/src/components/sidebar/NoticeHostGlyph.tsx +++ b/src/renderer/src/components/sidebar/NoticeHostGlyph.tsx @@ -9,6 +9,7 @@ import { translate } from '@/i18n/i18n' type NoticeHostGlyphProps = { hostId: ExecutionHostId hostLabel: string + keyboardFocusable: boolean } /** @@ -21,7 +22,8 @@ type NoticeHostGlyphProps = { */ export default function NoticeHostGlyph({ hostId, - hostLabel + hostLabel, + keyboardFocusable }: NoticeHostGlyphProps): React.JSX.Element | null { const host = parseExecutionHostId(hostId) const isDisconnected = useAppStore((s) => { @@ -61,7 +63,13 @@ export default function NoticeHostGlyph({ return ( - +