mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(sidebar): make notice host glyphs accessible
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -159,7 +159,11 @@ export default function ImportedWorktreesVisibilityLine({
|
||||
{hostContextLabel && placement !== 'pinned-fallback' ? (
|
||||
<span className="inline-flex min-w-0 shrink items-center gap-1">
|
||||
{hostContextHostId ? (
|
||||
<NoticeHostGlyph hostId={hostContextHostId} hostLabel={hostContextLabel} />
|
||||
<NoticeHostGlyph
|
||||
hostId={hostContextHostId}
|
||||
hostLabel={hostContextLabel}
|
||||
keyboardFocusable
|
||||
/>
|
||||
) : null}
|
||||
<span className="min-w-0 truncate text-[10px] leading-none text-muted-foreground">
|
||||
{hostContextLabel}
|
||||
|
||||
@@ -103,7 +103,11 @@ export default function NewExternalWorktreesInboxLine({
|
||||
{hostContextLabel ? (
|
||||
<span className="inline-flex min-w-0 shrink items-center gap-1">
|
||||
{hostContextHostId ? (
|
||||
<NoticeHostGlyph hostId={hostContextHostId} hostLabel={hostContextLabel} />
|
||||
<NoticeHostGlyph
|
||||
hostId={hostContextHostId}
|
||||
hostLabel={hostContextLabel}
|
||||
keyboardFocusable={false}
|
||||
/>
|
||||
) : null}
|
||||
<span className="min-w-0 truncate text-[10px] leading-none text-muted-foreground">
|
||||
{hostContextLabel}
|
||||
|
||||
@@ -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<string, { status?: unknown }>()
|
||||
|
||||
@@ -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 }) => (
|
||||
<span data-testid="tooltip">{children}</span>
|
||||
)
|
||||
@@ -28,13 +32,23 @@ vi.mock('@/components/ui/tooltip', () => ({
|
||||
|
||||
const roots: Root[] = []
|
||||
|
||||
async function render(hostId: string, hostLabel = 'openclaw'): Promise<HTMLDivElement> {
|
||||
async function render(
|
||||
hostId: string,
|
||||
hostLabel = 'openclaw',
|
||||
keyboardFocusable = false
|
||||
): Promise<HTMLDivElement> {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
const root = createRoot(container)
|
||||
roots.push(root)
|
||||
await act(async () => {
|
||||
root.render(<NoticeHostGlyph hostId={hostId as never} hostLabel={hostLabel} />)
|
||||
root.render(
|
||||
<NoticeHostGlyph
|
||||
hostId={hostId as never}
|
||||
hostLabel={hostLabel}
|
||||
keyboardFocusable={keyboardFocusable}
|
||||
/>
|
||||
)
|
||||
})
|
||||
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}}')
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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 (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex shrink-0 items-center" data-notice-host-kind={host.kind}>
|
||||
<span
|
||||
aria-label={keyboardFocusable ? tooltip : undefined}
|
||||
className="inline-flex shrink-0 items-center rounded-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-worktree-sidebar-ring"
|
||||
data-notice-host-kind={host.kind}
|
||||
role={keyboardFocusable ? 'img' : undefined}
|
||||
tabIndex={keyboardFocusable ? 0 : undefined}
|
||||
>
|
||||
<HostRowIcon
|
||||
hostId={hostId}
|
||||
className={`size-3 shrink-0 ${
|
||||
|
||||
@@ -5059,6 +5059,12 @@
|
||||
"5e1b8d3f62": "Ocultar worktrees externos permanentemente",
|
||||
"c3e8a1f4b2": "No volver a mostrar"
|
||||
},
|
||||
"NoticeHostGlyph": {
|
||||
"hostDisconnected": "{{hostName}} está desconectado",
|
||||
"sshHostProject": "Proyecto en el host SSH {{hostName}}",
|
||||
"localHostProject": "Proyecto en este host",
|
||||
"runtimeHostProject": "Proyecto en {{hostName}}"
|
||||
},
|
||||
"newExternalWorktreesInboxActions": {
|
||||
"a11c2f6d89": "No se pudieron mantener ocultos los worktrees externos. Inténtalo de nuevo.",
|
||||
"b7e4d1a062": "No se pudieron importar los worktrees externos. Inténtalo de nuevo.",
|
||||
|
||||
@@ -5059,6 +5059,12 @@
|
||||
"5e1b8d3f62": "外部ワークツリーを完全に非表示にする",
|
||||
"c3e8a1f4b2": "今後表示しない"
|
||||
},
|
||||
"NoticeHostGlyph": {
|
||||
"hostDisconnected": "{{hostName}} が切断されました",
|
||||
"sshHostProject": "SSH ホスト {{hostName}} 上のプロジェクト",
|
||||
"localHostProject": "このホスト上のプロジェクト",
|
||||
"runtimeHostProject": "{{hostName}} 上のプロジェクト"
|
||||
},
|
||||
"newExternalWorktreesInboxActions": {
|
||||
"a11c2f6d89": "外部ワークツリーを非表示のままにできませんでした。もう一度お試しください。",
|
||||
"b7e4d1a062": "外部ワークツリーをインポートできませんでした。もう一度お試しください。",
|
||||
|
||||
@@ -5061,6 +5061,12 @@
|
||||
"5e1b8d3f62": "외부 워크트리를 영구적으로 숨기기",
|
||||
"c3e8a1f4b2": "다시 표시하지 않기"
|
||||
},
|
||||
"NoticeHostGlyph": {
|
||||
"hostDisconnected": "{{hostName}} 연결 끊김",
|
||||
"sshHostProject": "SSH 호스트 {{hostName}}의 프로젝트",
|
||||
"localHostProject": "이 호스트의 프로젝트",
|
||||
"runtimeHostProject": "{{hostName}}의 프로젝트"
|
||||
},
|
||||
"newExternalWorktreesInboxActions": {
|
||||
"a11c2f6d89": "외부 워크트리를 숨김 상태로 유지할 수 없습니다. 다시 시도하세요.",
|
||||
"b7e4d1a062": "외부 워크트리를 가져올 수 없습니다. 다시 시도하세요.",
|
||||
|
||||
@@ -5071,6 +5071,12 @@
|
||||
"5e1b8d3f62": "永久隐藏外部工作树",
|
||||
"c3e8a1f4b2": "不再显示"
|
||||
},
|
||||
"NoticeHostGlyph": {
|
||||
"hostDisconnected": "{{hostName}} 已断开连接",
|
||||
"sshHostProject": "SSH 主机 {{hostName}} 上的项目",
|
||||
"localHostProject": "此主机上的项目",
|
||||
"runtimeHostProject": "{{hostName}} 上的项目"
|
||||
},
|
||||
"newExternalWorktreesInboxActions": {
|
||||
"a11c2f6d89": "无法保持外部工作树隐藏。请重试。",
|
||||
"b7e4d1a062": "无法导入外部工作树。请重试。",
|
||||
|
||||
Reference in New Issue
Block a user