@stigmer/react 3.1.2 → 3.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/composer/SessionComposer.d.ts +37 -5
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +13 -5
- package/composer/SessionComposer.js.map +1 -1
- package/execution/ArtifactApplyButton.d.ts +25 -0
- package/execution/ArtifactApplyButton.d.ts.map +1 -0
- package/execution/ArtifactApplyButton.js +21 -0
- package/execution/ArtifactApplyButton.js.map +1 -0
- package/execution/ArtifactContentBody.d.ts +36 -0
- package/execution/ArtifactContentBody.d.ts.map +1 -0
- package/execution/ArtifactContentBody.js +62 -0
- package/execution/ArtifactContentBody.js.map +1 -0
- package/execution/ArtifactDocument.d.ts +48 -0
- package/execution/ArtifactDocument.d.ts.map +1 -0
- package/execution/ArtifactDocument.js +89 -0
- package/execution/ArtifactDocument.js.map +1 -0
- package/execution/ArtifactPreviewModal.d.ts +9 -9
- package/execution/ArtifactPreviewModal.d.ts.map +1 -1
- package/execution/ArtifactPreviewModal.js +18 -145
- package/execution/ArtifactPreviewModal.js.map +1 -1
- package/execution/ArtifactRow.d.ts +49 -0
- package/execution/ArtifactRow.d.ts.map +1 -0
- package/execution/ArtifactRow.js +56 -0
- package/execution/ArtifactRow.js.map +1 -0
- package/execution/ArtifactsWidget.d.ts +6 -9
- package/execution/ArtifactsWidget.d.ts.map +1 -1
- package/execution/ArtifactsWidget.js +15 -19
- package/execution/ArtifactsWidget.js.map +1 -1
- package/execution/FileChangeProgressBar.d.ts +47 -0
- package/execution/FileChangeProgressBar.d.ts.map +1 -0
- package/execution/FileChangeProgressBar.js +49 -0
- package/execution/FileChangeProgressBar.js.map +1 -0
- package/execution/FileChangesView.d.ts.map +1 -1
- package/execution/FileChangesView.js +19 -4
- package/execution/FileChangesView.js.map +1 -1
- package/execution/FileReviewAtoms.d.ts +27 -0
- package/execution/FileReviewAtoms.d.ts.map +1 -0
- package/execution/FileReviewAtoms.js +64 -0
- package/execution/FileReviewAtoms.js.map +1 -0
- package/execution/FileReviewCard.d.ts +2 -2
- package/execution/FileReviewCard.d.ts.map +1 -1
- package/execution/FileReviewCard.js +32 -62
- package/execution/FileReviewCard.js.map +1 -1
- package/execution/MessageEntry.d.ts +0 -9
- package/execution/MessageEntry.d.ts.map +1 -1
- package/execution/MessageEntry.js +1 -11
- package/execution/MessageEntry.js.map +1 -1
- package/execution/MessageThread.d.ts +3 -4
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +25 -11
- package/execution/MessageThread.js.map +1 -1
- package/execution/PlanArtifactCard.d.ts +16 -10
- package/execution/PlanArtifactCard.d.ts.map +1 -1
- package/execution/PlanArtifactCard.js +31 -7
- package/execution/PlanArtifactCard.js.map +1 -1
- package/execution/PlanCompletionCard.d.ts +4 -4
- package/execution/PlanCompletionCard.d.ts.map +1 -1
- package/execution/PlanCompletionCard.js +4 -4
- package/execution/PlanCompletionCard.js.map +1 -1
- package/execution/PlanStreamingCard.d.ts +7 -2
- package/execution/PlanStreamingCard.d.ts.map +1 -1
- package/execution/PlanStreamingCard.js +11 -6
- package/execution/PlanStreamingCard.js.map +1 -1
- package/execution/index.d.ts +12 -2
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +6 -1
- package/execution/index.js.map +1 -1
- package/execution/use-build-from-plan-hotkey.d.ts +1 -1
- package/execution/use-build-from-plan-hotkey.js +1 -1
- package/execution/useArtifactContent.d.ts +6 -1
- package/execution/useArtifactContent.d.ts.map +1 -1
- package/execution/useArtifactContent.js +7 -2
- package/execution/useArtifactContent.js.map +1 -1
- package/execution/useArtifactCopy.d.ts +51 -0
- package/execution/useArtifactCopy.d.ts.map +1 -0
- package/execution/useArtifactCopy.js +75 -0
- package/execution/useArtifactCopy.js.map +1 -0
- package/execution/useArtifactDownload.d.ts.map +1 -1
- package/execution/useArtifactDownload.js +17 -5
- package/execution/useArtifactDownload.js.map +1 -1
- package/execution/useArtifactInspection.d.ts +85 -0
- package/execution/useArtifactInspection.d.ts.map +1 -0
- package/execution/useArtifactInspection.js +143 -0
- package/execution/useArtifactInspection.js.map +1 -0
- package/execution/useCreateAgentExecution.d.ts +16 -3
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js +1 -0
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/index.d.ts +4 -4
- package/index.d.ts.map +1 -1
- package/index.js +3 -3
- package/index.js.map +1 -1
- package/internal/MermaidDiagram.d.ts +47 -0
- package/internal/MermaidDiagram.d.ts.map +1 -0
- package/internal/MermaidDiagram.js +118 -0
- package/internal/MermaidDiagram.js.map +1 -0
- package/internal/file-tree/FileTreeNode.d.ts.map +1 -1
- package/internal/file-tree/FileTreeNode.js +2 -2
- package/internal/file-tree/FileTreeNode.js.map +1 -1
- package/internal/markdown-components.d.ts.map +1 -1
- package/internal/markdown-components.js +34 -0
- package/internal/markdown-components.js.map +1 -1
- package/internal/mermaid-loader.d.ts +26 -0
- package/internal/mermaid-loader.d.ts.map +1 -0
- package/internal/mermaid-loader.js +34 -0
- package/internal/mermaid-loader.js.map +1 -0
- package/internal/mermaid-theme-css.d.ts +41 -0
- package/internal/mermaid-theme-css.d.ts.map +1 -0
- package/internal/mermaid-theme-css.js +179 -0
- package/internal/mermaid-theme-css.js.map +1 -0
- package/internal/slug.d.ts +9 -5
- package/internal/slug.d.ts.map +1 -1
- package/internal/slug.js +10 -6
- package/internal/slug.js.map +1 -1
- package/library/detect-plan-artifact.d.ts +27 -9
- package/library/detect-plan-artifact.d.ts.map +1 -1
- package/library/detect-plan-artifact.js +26 -6
- package/library/detect-plan-artifact.js.map +1 -1
- package/library/index.d.ts +1 -1
- package/library/index.d.ts.map +1 -1
- package/library/index.js +1 -1
- package/library/index.js.map +1 -1
- package/package.json +5 -4
- package/session/PlanEditor.js +1 -1
- package/session/PlanEditor.js.map +1 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +122 -30
- package/session/SessionViewer.js.map +1 -1
- package/session/artifact-document.d.ts +13 -0
- package/session/artifact-document.d.ts.map +1 -0
- package/session/artifact-document.js +22 -0
- package/session/artifact-document.js.map +1 -0
- package/session/facets/ArtifactsTab.d.ts +30 -4
- package/session/facets/ArtifactsTab.d.ts.map +1 -1
- package/session/facets/ArtifactsTab.js +32 -10
- package/session/facets/ArtifactsTab.js.map +1 -1
- package/session/index.d.ts +2 -1
- package/session/index.d.ts.map +1 -1
- package/session/index.js +4 -1
- package/session/index.js.map +1 -1
- package/session/plan-document.d.ts +14 -2
- package/session/plan-document.d.ts.map +1 -1
- package/session/plan-document.js +14 -3
- package/session/plan-document.js.map +1 -1
- package/session/usePlanDraft.d.ts +1 -1
- package/session/useSessionArtifacts.d.ts +17 -0
- package/session/useSessionArtifacts.d.ts.map +1 -1
- package/session/useSessionArtifacts.js +16 -5
- package/session/useSessionArtifacts.js.map +1 -1
- package/session/useSessionConversation.d.ts +26 -3
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +51 -6
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +1 -0
- package/session/useSessionPageFlow.js.map +1 -1
- package/session/useSessionPanel.d.ts +20 -0
- package/session/useSessionPanel.d.ts.map +1 -1
- package/session/useSessionPanel.js +13 -0
- package/session/useSessionPanel.js.map +1 -1
- package/session/useSessionRailViews.d.ts +13 -1
- package/session/useSessionRailViews.d.ts.map +1 -1
- package/session/useSessionRailViews.js +5 -3
- package/session/useSessionRailViews.js.map +1 -1
- package/src/composer/SessionComposer.tsx +76 -7
- package/src/composer/__tests__/SessionComposer-editing.test.tsx +128 -0
- package/src/execution/ArtifactApplyButton.tsx +79 -0
- package/src/execution/ArtifactContentBody.tsx +255 -0
- package/src/execution/ArtifactDocument.tsx +344 -0
- package/src/execution/ArtifactPreviewModal.tsx +49 -415
- package/src/execution/ArtifactRow.tsx +165 -0
- package/src/execution/ArtifactsWidget.tsx +24 -33
- package/src/execution/FileChangeProgressBar.tsx +121 -0
- package/src/execution/FileChangesView.tsx +18 -4
- package/src/execution/FileReviewAtoms.tsx +89 -0
- package/src/execution/FileReviewCard.tsx +57 -90
- package/src/execution/MessageEntry.tsx +0 -62
- package/src/execution/MessageThread.tsx +29 -15
- package/src/execution/PlanArtifactCard.tsx +130 -52
- package/src/execution/PlanCompletionCard.tsx +5 -5
- package/src/execution/PlanStreamingCard.tsx +15 -11
- package/src/execution/__tests__/ArtifactContentBody.test.tsx +131 -0
- package/src/execution/__tests__/ArtifactDocument.test.tsx +100 -0
- package/src/execution/__tests__/ArtifactPreviewModal.test.tsx +6 -6
- package/src/execution/__tests__/ArtifactRow.test.tsx +139 -0
- package/src/execution/__tests__/ArtifactsWidget.test.tsx +78 -0
- package/src/execution/__tests__/FileChangeProgressBar.test.tsx +125 -0
- package/src/execution/__tests__/FileReviewCard.test.tsx +66 -7
- package/src/execution/__tests__/MessageThread.test.tsx +1 -1
- package/src/execution/__tests__/PlanArtifactCard.test.tsx +52 -18
- package/src/execution/__tests__/PlanCompletionCard.test.tsx +2 -2
- package/src/execution/__tests__/PlanStreamingCard.test.tsx +11 -4
- package/src/execution/__tests__/buildThreadItems-plan-completion.test.ts +104 -8
- package/src/execution/__tests__/message-entry.test.tsx +56 -39
- package/src/execution/__tests__/useArtifactCopy.test.tsx +90 -0
- package/src/execution/__tests__/useArtifactDownload.test.tsx +2 -0
- package/src/execution/__tests__/useArtifactInspection.test.tsx +151 -0
- package/src/execution/__tests__/useCreateAgentExecution.test.tsx +35 -0
- package/src/execution/index.ts +20 -2
- package/src/execution/use-build-from-plan-hotkey.ts +1 -1
- package/src/execution/useArtifactContent.ts +7 -0
- package/src/execution/useArtifactCopy.ts +109 -0
- package/src/execution/useArtifactDownload.ts +17 -5
- package/src/execution/useArtifactInspection.ts +252 -0
- package/src/execution/useCreateAgentExecution.ts +17 -3
- package/src/index.ts +17 -2
- package/src/internal/MermaidDiagram.tsx +164 -0
- package/src/internal/__tests__/a11y/mermaid-theme.a11y.test.tsx +122 -0
- package/src/internal/__tests__/markdown-components.test.tsx +93 -1
- package/src/internal/__tests__/mermaid-diagram.test.tsx +282 -0
- package/src/internal/file-tree/FileTreeNode.tsx +13 -2
- package/src/internal/markdown-components.tsx +36 -1
- package/src/internal/mermaid-loader.ts +44 -0
- package/src/internal/mermaid-theme-css.ts +178 -0
- package/src/internal/slug.ts +10 -6
- package/src/library/__tests__/detect-plan-artifact.test.ts +21 -0
- package/src/library/detect-plan-artifact.ts +32 -10
- package/src/library/index.ts +2 -0
- package/src/session/PlanEditor.tsx +1 -1
- package/src/session/SessionViewer.tsx +227 -51
- package/src/session/__tests__/PlanEditor.test.tsx +4 -4
- package/src/session/__tests__/PlanStreamingDocument.test.tsx +1 -1
- package/src/session/__tests__/SessionViewer-artifactTabs.test.tsx +210 -0
- package/src/session/__tests__/SessionViewer-editResubmit.test.tsx +233 -0
- package/src/session/__tests__/SessionViewer-fileReviewDock.test.tsx +57 -0
- package/src/session/__tests__/useSessionArtifacts.test.ts +57 -0
- package/src/session/__tests__/useSessionConversation.test.tsx +216 -1
- package/src/session/__tests__/useSessionPanel.test.ts +70 -1
- package/src/session/artifact-document.ts +23 -0
- package/src/session/facets/ArtifactsTab.tsx +66 -18
- package/src/session/facets/__tests__/ArtifactsTab.test.tsx +111 -47
- package/src/session/index.ts +5 -1
- package/src/session/plan-document.ts +14 -3
- package/src/session/usePlanDraft.ts +1 -1
- package/src/session/useSessionArtifacts.ts +16 -5
- package/src/session/useSessionConversation.ts +80 -10
- package/src/session/useSessionPageFlow.ts +1 -0
- package/src/session/useSessionPanel.ts +41 -0
- package/src/session/useSessionRailViews.tsx +21 -1
- package/src/workspace/EditorTabs.tsx +38 -9
- package/src/workspace/ExplorerTree.tsx +10 -4
- package/src/workspace/FileViewer.tsx +27 -6
- package/src/workspace/WorkspaceContentSearch.tsx +44 -20
- package/src/workspace/WorkspaceEditor.tsx +19 -16
- package/src/workspace/WorkspaceFileSearch.tsx +51 -28
- package/src/workspace/WorkspaceSurface.tsx +106 -48
- package/src/workspace/__tests__/EditorTabs.test.tsx +4 -2
- package/src/workspace/__tests__/ExplorerTree.test.tsx +52 -0
- package/src/workspace/__tests__/FileViewer.test.tsx +18 -0
- package/src/workspace/__tests__/WorkspaceContentSearch.test.tsx +38 -5
- package/src/workspace/__tests__/WorkspaceEditor-initialPanel.test.tsx +19 -0
- package/src/workspace/__tests__/WorkspaceFileSearch.test.tsx +27 -6
- package/src/workspace/__tests__/WorkspaceSurface.test.tsx +70 -0
- package/src/workspace/__tests__/a11y/harness.tsx +140 -0
- package/src/workspace/__tests__/a11y/mock-capabilities.ts +138 -0
- package/src/workspace/__tests__/a11y/workspace-editor.a11y.test.tsx +65 -0
- package/src/workspace/__tests__/a11y/workspace-explorer-search.a11y.test.tsx +112 -0
- package/src/workspace/__tests__/a11y/workspace-viewer-tabs.a11y.test.tsx +69 -0
- package/styles.css +1 -1
- package/workspace/EditorTabs.d.ts +21 -3
- package/workspace/EditorTabs.d.ts.map +1 -1
- package/workspace/EditorTabs.js +18 -6
- package/workspace/EditorTabs.js.map +1 -1
- package/workspace/ExplorerTree.js +8 -4
- package/workspace/ExplorerTree.js.map +1 -1
- package/workspace/FileViewer.d.ts.map +1 -1
- package/workspace/FileViewer.js +19 -8
- package/workspace/FileViewer.js.map +1 -1
- package/workspace/WorkspaceContentSearch.d.ts.map +1 -1
- package/workspace/WorkspaceContentSearch.js +23 -2
- package/workspace/WorkspaceContentSearch.js.map +1 -1
- package/workspace/WorkspaceEditor.d.ts.map +1 -1
- package/workspace/WorkspaceEditor.js +8 -10
- package/workspace/WorkspaceEditor.js.map +1 -1
- package/workspace/WorkspaceFileSearch.d.ts.map +1 -1
- package/workspace/WorkspaceFileSearch.js +23 -3
- package/workspace/WorkspaceFileSearch.js.map +1 -1
- package/workspace/WorkspaceSurface.d.ts.map +1 -1
- package/workspace/WorkspaceSurface.js +54 -18
- package/workspace/WorkspaceSurface.js.map +1 -1
- package/execution/ArtifactCard.d.ts +0 -81
- package/execution/ArtifactCard.d.ts.map +0 -1
- package/execution/ArtifactCard.js +0 -130
- package/execution/ArtifactCard.js.map +0 -1
- package/src/execution/ArtifactCard.tsx +0 -320
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
useCallback,
|
|
5
|
+
useId,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type KeyboardEvent,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
4
11
|
import { cn } from "@stigmer/theme";
|
|
5
12
|
import type { FileChange } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
6
13
|
import { ResizableSplit } from "../internal/ResizableSplit.js";
|
|
@@ -18,7 +25,7 @@ import type { WorkspaceContentSearcher } from "./WorkspaceContentSearcher.js";
|
|
|
18
25
|
import { WorkspaceFileSearch } from "./WorkspaceFileSearch.js";
|
|
19
26
|
import { WorkspaceContentSearch } from "./WorkspaceContentSearch.js";
|
|
20
27
|
import { FileViewer, type FileViewerHandle } from "./FileViewer.js";
|
|
21
|
-
import { EditorTabs } from "./EditorTabs.js";
|
|
28
|
+
import { EditorTabs, editorTabDomId } from "./EditorTabs.js";
|
|
22
29
|
import { ExplorerTree } from "./ExplorerTree.js";
|
|
23
30
|
|
|
24
31
|
/** The built-in rail views every surface has. */
|
|
@@ -302,8 +309,14 @@ function ActivityRail({
|
|
|
302
309
|
...(extraViews ?? []),
|
|
303
310
|
];
|
|
304
311
|
|
|
305
|
-
//
|
|
306
|
-
|
|
312
|
+
// One ref per rail button so selection can move DOM focus with it (below).
|
|
313
|
+
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
|
314
|
+
|
|
315
|
+
// Roving radiogroup: selection follows focus. `keydown` fires on the focused
|
|
316
|
+
// button, so its `index` is the current position; we select the neighbour AND
|
|
317
|
+
// move focus to it. Moving focus is essential — without it focus stays pinned
|
|
318
|
+
// on the entered button (whose index never changes) and every later view
|
|
319
|
+
// (Config/Changes/…) is unreachable by keyboard.
|
|
307
320
|
const handleKeyDown = useCallback(
|
|
308
321
|
(e: KeyboardEvent<HTMLButtonElement>, index: number) => {
|
|
309
322
|
let nextIndex: number | null = null;
|
|
@@ -311,10 +324,15 @@ function ActivityRail({
|
|
|
311
324
|
nextIndex = Math.min(index + 1, items.length - 1);
|
|
312
325
|
} else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
|
|
313
326
|
nextIndex = Math.max(index - 1, 0);
|
|
327
|
+
} else if (e.key === "Home") {
|
|
328
|
+
nextIndex = 0;
|
|
329
|
+
} else if (e.key === "End") {
|
|
330
|
+
nextIndex = items.length - 1;
|
|
314
331
|
}
|
|
315
332
|
if (nextIndex === null || nextIndex === index) return;
|
|
316
333
|
e.preventDefault();
|
|
317
334
|
onViewChange(items[nextIndex].id);
|
|
335
|
+
buttonRefs.current[nextIndex]?.focus();
|
|
318
336
|
},
|
|
319
337
|
[items, onViewChange],
|
|
320
338
|
);
|
|
@@ -332,6 +350,9 @@ function ActivityRail({
|
|
|
332
350
|
return (
|
|
333
351
|
<button
|
|
334
352
|
key={item.id}
|
|
353
|
+
ref={(el) => {
|
|
354
|
+
buttonRefs.current[index] = el;
|
|
355
|
+
}}
|
|
335
356
|
type="button"
|
|
336
357
|
role="radio"
|
|
337
358
|
aria-checked={isSelected}
|
|
@@ -487,14 +508,26 @@ function SearchModeToggle({
|
|
|
487
508
|
{ value: "text", label: "Text" },
|
|
488
509
|
];
|
|
489
510
|
|
|
511
|
+
// Selection follows focus, matching ActivityRail / ViewerModeToggle.
|
|
512
|
+
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
|
513
|
+
const selectMode = useCallback(
|
|
514
|
+
(next: SearchMode) => {
|
|
515
|
+
onChange(next);
|
|
516
|
+
buttonRefs.current[next === "name" ? 0 : 1]?.focus();
|
|
517
|
+
},
|
|
518
|
+
[onChange],
|
|
519
|
+
);
|
|
490
520
|
const handleKeyDown = useCallback(
|
|
491
521
|
(e: KeyboardEvent<HTMLButtonElement>) => {
|
|
492
|
-
if (e.key === "ArrowRight" || e.key === "
|
|
522
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
|
493
523
|
e.preventDefault();
|
|
494
|
-
|
|
524
|
+
selectMode("text");
|
|
525
|
+
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
|
526
|
+
e.preventDefault();
|
|
527
|
+
selectMode("name");
|
|
495
528
|
}
|
|
496
529
|
},
|
|
497
|
-
[
|
|
530
|
+
[selectMode],
|
|
498
531
|
);
|
|
499
532
|
|
|
500
533
|
return (
|
|
@@ -504,11 +537,14 @@ function SearchModeToggle({
|
|
|
504
537
|
aria-label="Search mode"
|
|
505
538
|
className="inline-flex rounded-md bg-muted p-0.5"
|
|
506
539
|
>
|
|
507
|
-
{options.map((option) => {
|
|
540
|
+
{options.map((option, index) => {
|
|
508
541
|
const isSelected = value === option.value;
|
|
509
542
|
return (
|
|
510
543
|
<button
|
|
511
544
|
key={option.value}
|
|
545
|
+
ref={(el) => {
|
|
546
|
+
buttonRefs.current[index] = el;
|
|
547
|
+
}}
|
|
512
548
|
type="button"
|
|
513
549
|
role="radio"
|
|
514
550
|
aria-checked={isSelected}
|
|
@@ -579,6 +615,10 @@ function EditorArea({
|
|
|
579
615
|
readonly onCollapse: () => void;
|
|
580
616
|
}) {
|
|
581
617
|
const viewerRef = useRef<FileViewerHandle>(null);
|
|
618
|
+
// Instance-scoped ids tie each tab to the single editor body (tabpanel).
|
|
619
|
+
const idBase = useId();
|
|
620
|
+
const panelId = `${idBase}editor-panel`;
|
|
621
|
+
const tabIdPrefix = `${idBase}editor-tab-`;
|
|
582
622
|
const activeKey = selectedFile
|
|
583
623
|
? editorKey(selectedFile.entryId, selectedFile.path)
|
|
584
624
|
: null;
|
|
@@ -616,51 +656,69 @@ function EditorArea({
|
|
|
616
656
|
onActivate={onActivateEditor}
|
|
617
657
|
onPin={onPinEditor}
|
|
618
658
|
onClose={onCloseEditor}
|
|
659
|
+
panelId={panelId}
|
|
660
|
+
tabIdPrefix={tabIdPrefix}
|
|
619
661
|
className="min-w-0 flex-1 border-b-0"
|
|
620
662
|
/>
|
|
621
663
|
)}
|
|
622
664
|
</div>
|
|
623
|
-
{
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
<FileViewer
|
|
647
|
-
ref={viewerRef}
|
|
665
|
+
{/* The single editor body — the one tabpanel the tab strip controls (the
|
|
666
|
+
content swaps; the panel is stable). role/labelledby apply only with an
|
|
667
|
+
active tab (the empty state has no tab to label). Carries the same
|
|
668
|
+
flex/min-w-0 chain as the branches so the DD-20 reflow is preserved:
|
|
669
|
+
the wrapper is just a labelled passthrough, not a new layout context. */}
|
|
670
|
+
<div
|
|
671
|
+
className="flex min-h-0 min-w-0 flex-1 flex-col"
|
|
672
|
+
{...(activeKey
|
|
673
|
+
? {
|
|
674
|
+
role: "tabpanel",
|
|
675
|
+
id: panelId,
|
|
676
|
+
"aria-labelledby": editorTabDomId(tabIdPrefix, activeKey),
|
|
677
|
+
}
|
|
678
|
+
: {})}
|
|
679
|
+
>
|
|
680
|
+
{activeVirtualDocument ? (
|
|
681
|
+
// A virtual document owns its body wholesale: no breadcrumbs (there is
|
|
682
|
+
// no filesystem location) and no FileViewer (there is no reader-backed
|
|
683
|
+
// file). Keyed like files so switching documents resets cleanly.
|
|
684
|
+
// Vertical-only scrolling: a lone overflow-y-auto would compute
|
|
685
|
+
// overflow-x to auto too, giving the pane a horizontal scrollbar the
|
|
686
|
+
// moment any child refuses to shrink — documents must reflow instead.
|
|
687
|
+
<div
|
|
648
688
|
key={activeKey ?? undefined}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
689
|
+
className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden"
|
|
690
|
+
>
|
|
691
|
+
{activeVirtualDocument.content}
|
|
692
|
+
</div>
|
|
693
|
+
) : selectedFile ? (
|
|
694
|
+
<>
|
|
695
|
+
<Breadcrumbs
|
|
696
|
+
entryName={activeEntryName}
|
|
697
|
+
path={selectedFile.path}
|
|
698
|
+
onRefresh={reader ? () => viewerRef.current?.refresh() : undefined}
|
|
699
|
+
/>
|
|
700
|
+
{/* Per-file remount (key) resets scroll and view-mode state cleanly.
|
|
701
|
+
The tab strip + breadcrumbs own the file identity, close, and
|
|
702
|
+
refresh, so the viewer is chrome-less; Escape still collapses. */}
|
|
703
|
+
<FileViewer
|
|
704
|
+
ref={viewerRef}
|
|
705
|
+
key={activeKey ?? undefined}
|
|
706
|
+
selectedFile={selectedFile}
|
|
707
|
+
entries={entries}
|
|
708
|
+
reader={reader}
|
|
709
|
+
change={change}
|
|
710
|
+
reveal={reveal}
|
|
711
|
+
onClose={onCollapse}
|
|
712
|
+
showHeader={false}
|
|
713
|
+
className="min-h-0 flex-1"
|
|
714
|
+
/>
|
|
715
|
+
</>
|
|
716
|
+
) : (
|
|
717
|
+
<div className="flex flex-1 items-center justify-center p-8 text-center text-xs text-muted-foreground">
|
|
718
|
+
Select a file to view its contents.
|
|
719
|
+
</div>
|
|
720
|
+
)}
|
|
721
|
+
</div>
|
|
664
722
|
</div>
|
|
665
723
|
);
|
|
666
724
|
}
|
|
@@ -67,9 +67,11 @@ describe("EditorTabs", () => {
|
|
|
67
67
|
expect(onPin).toHaveBeenCalledWith("e1", "src/b.ts");
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
it("closes a tab via its close
|
|
70
|
+
it("closes a tab via its close affordance (presentational, aria-hidden)", () => {
|
|
71
71
|
const { onClose } = renderTabs();
|
|
72
|
-
|
|
72
|
+
// The close "X" is a presentational span (not a nested button) to satisfy
|
|
73
|
+
// WCAG 4.1.2; it is addressable by its title.
|
|
74
|
+
fireEvent.click(screen.getByTitle("Close a.ts"));
|
|
73
75
|
expect(onClose).toHaveBeenCalledWith("e1", "src/a.ts");
|
|
74
76
|
});
|
|
75
77
|
|
|
@@ -149,4 +149,56 @@ describe("ExplorerTree", () => {
|
|
|
149
149
|
screen.queryByRole("button", { name: /Remove .* from workspace/ }),
|
|
150
150
|
).toBeNull();
|
|
151
151
|
});
|
|
152
|
+
|
|
153
|
+
it("exposes tree depth via aria-level and keeps aria-expanded on the treeitem", async () => {
|
|
154
|
+
render(
|
|
155
|
+
<ExplorerTree
|
|
156
|
+
entries={[entry("x9")]}
|
|
157
|
+
lister={lister(["src/nested.ts"])}
|
|
158
|
+
selectedFile={null}
|
|
159
|
+
onOpenFile={vi.fn()}
|
|
160
|
+
onActivateFile={vi.fn()}
|
|
161
|
+
/>,
|
|
162
|
+
);
|
|
163
|
+
const folder = await screen.findByText("src");
|
|
164
|
+
const folderItem = folder.closest('[role="treeitem"]')!;
|
|
165
|
+
// Top-level treeitem is level 1; expandability is announced on the treeitem.
|
|
166
|
+
expect(folderItem.getAttribute("aria-level")).toBe("1");
|
|
167
|
+
expect(folderItem.getAttribute("aria-expanded")).toBe("false");
|
|
168
|
+
|
|
169
|
+
fireEvent.click(folder);
|
|
170
|
+
const nested = await screen.findByText("nested.ts");
|
|
171
|
+
expect(nested.closest('[role="treeitem"]')!.getAttribute("aria-level")).toBe(
|
|
172
|
+
"2",
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("shows a substrate-agnostic truncation banner from a notice entry (DD-11)", async () => {
|
|
177
|
+
// A lister that signals an incomplete listing via a `notice` entry (as the
|
|
178
|
+
// desktop and GitHub listers do). The advisory is stripped from the tree and
|
|
179
|
+
// surfaced as a banner instead.
|
|
180
|
+
const truncatedLister: WorkspaceFileLister = vi.fn(async () => [
|
|
181
|
+
{ path: "a.ts", isDirectory: false },
|
|
182
|
+
{
|
|
183
|
+
path: "... (listing truncated)",
|
|
184
|
+
isDirectory: false,
|
|
185
|
+
notice: true as const,
|
|
186
|
+
},
|
|
187
|
+
]);
|
|
188
|
+
render(
|
|
189
|
+
<ExplorerTree
|
|
190
|
+
entries={[entry("x8")]}
|
|
191
|
+
lister={truncatedLister}
|
|
192
|
+
selectedFile={null}
|
|
193
|
+
onOpenFile={vi.fn()}
|
|
194
|
+
onActivateFile={vi.fn()}
|
|
195
|
+
/>,
|
|
196
|
+
);
|
|
197
|
+
const banner = await screen.findByText(/too many files to load in full/i);
|
|
198
|
+
expect(banner).toBeTruthy();
|
|
199
|
+
// Wording must read correctly for a local folder, not just a git repo.
|
|
200
|
+
expect(banner.textContent).not.toMatch(/repository/i);
|
|
201
|
+
// The notice advisory is never an openable tree leaf.
|
|
202
|
+
expect(screen.queryByText("... (listing truncated)")).toBeNull();
|
|
203
|
+
});
|
|
152
204
|
});
|
|
@@ -326,6 +326,24 @@ describe("FileViewer — diff mode", () => {
|
|
|
326
326
|
expect(screen.getByRole("radio", { name: "Diff" }).getAttribute("aria-checked")).toBe("true"),
|
|
327
327
|
);
|
|
328
328
|
});
|
|
329
|
+
|
|
330
|
+
it("moves focus with selection when roving Diff|File", async () => {
|
|
331
|
+
render(
|
|
332
|
+
<FileViewer
|
|
333
|
+
selectedFile={{ entryId: "e1", path: "src/a.ts" }}
|
|
334
|
+
entries={ENTRIES}
|
|
335
|
+
reader={readerFor(text("live content"))}
|
|
336
|
+
change={wholeFileChange("src/a.ts")}
|
|
337
|
+
/>,
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const diffRadio = screen.getByRole("radio", { name: "Diff" });
|
|
341
|
+
diffRadio.focus();
|
|
342
|
+
fireEvent.keyDown(diffRadio, { key: "ArrowRight" });
|
|
343
|
+
await waitFor(() =>
|
|
344
|
+
expect(document.activeElement).toBe(screen.getByRole("radio", { name: "File" })),
|
|
345
|
+
);
|
|
346
|
+
});
|
|
329
347
|
});
|
|
330
348
|
|
|
331
349
|
// ---------------------------------------------------------------------------
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
-
import { render, screen, fireEvent, cleanup
|
|
2
|
+
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
|
3
3
|
import { WorkspaceContentSearch } from "../WorkspaceContentSearch";
|
|
4
4
|
import type { WorkspaceEntry } from "../useWorkspaceEntries";
|
|
5
5
|
import type {
|
|
@@ -69,7 +69,10 @@ describe("WorkspaceContentSearch", () => {
|
|
|
69
69
|
<WorkspaceContentSearch entries={[makeEntry()]} searcher={searcher} onOpenFile={vi.fn()} />,
|
|
70
70
|
);
|
|
71
71
|
typeQuery("a");
|
|
72
|
-
|
|
72
|
+
// Assert the visible message, not the parallel sr-only live-region copy.
|
|
73
|
+
expect(
|
|
74
|
+
screen.getByText(/type at least 2 characters/i, { ignore: ".sr-only" }),
|
|
75
|
+
).toBeTruthy();
|
|
73
76
|
expect(searcher).not.toHaveBeenCalled();
|
|
74
77
|
});
|
|
75
78
|
|
|
@@ -96,7 +99,7 @@ describe("WorkspaceContentSearch", () => {
|
|
|
96
99
|
// Clicking a line hit opens via the shared onOpenFile seam (the same path a
|
|
97
100
|
// tree/filename click uses — this is what makes a changed file honor the
|
|
98
101
|
// FileViewer diff-default; see FileViewer diff-mode suite).
|
|
99
|
-
fireEvent.click(
|
|
102
|
+
fireEvent.click(options[0]);
|
|
100
103
|
// The hit's line rides along so the viewer jumps to it (DR-1/DR-2).
|
|
101
104
|
expect(onOpenFile).toHaveBeenCalledWith(entry.id, "src/app.ts", { line: 3 });
|
|
102
105
|
});
|
|
@@ -138,7 +141,9 @@ describe("WorkspaceContentSearch", () => {
|
|
|
138
141
|
/>,
|
|
139
142
|
);
|
|
140
143
|
typeQuery("zzz");
|
|
141
|
-
expect(
|
|
144
|
+
expect(
|
|
145
|
+
await screen.findByText(/no files containing/i, { ignore: ".sr-only" }),
|
|
146
|
+
).toBeTruthy();
|
|
142
147
|
});
|
|
143
148
|
|
|
144
149
|
it("moves virtual focus with ArrowDown and opens on Enter", async () => {
|
|
@@ -178,7 +183,35 @@ describe("WorkspaceContentSearch", () => {
|
|
|
178
183
|
typeQuery("foo");
|
|
179
184
|
await screen.findAllByRole("option");
|
|
180
185
|
expect(screen.getAllByRole("option")).toHaveLength(200);
|
|
181
|
-
expect(
|
|
186
|
+
expect(
|
|
187
|
+
screen.getByText(/showing the first 200 of 250 matches/i, {
|
|
188
|
+
ignore: ".sr-only",
|
|
189
|
+
}),
|
|
190
|
+
).toBeTruthy();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("announces result status in a polite live region", async () => {
|
|
194
|
+
// Content matching is backend-delegated, so the searcher must vary by query.
|
|
195
|
+
const searcher: WorkspaceContentSearcher = vi.fn(async (_e, query) =>
|
|
196
|
+
query === "foo" ? ok([match("a.ts", 1, "foo")]) : ok([]),
|
|
197
|
+
);
|
|
198
|
+
render(
|
|
199
|
+
<WorkspaceContentSearch
|
|
200
|
+
entries={[makeEntry()]}
|
|
201
|
+
searcher={searcher}
|
|
202
|
+
onOpenFile={vi.fn()}
|
|
203
|
+
/>,
|
|
204
|
+
);
|
|
205
|
+
const status = screen.getByRole("status");
|
|
206
|
+
expect(status.getAttribute("aria-live")).toBe("polite");
|
|
207
|
+
|
|
208
|
+
typeQuery("foo");
|
|
209
|
+
await screen.findByRole("option");
|
|
210
|
+
expect(status.textContent).toMatch(/match/i);
|
|
211
|
+
|
|
212
|
+
typeQuery("zzz");
|
|
213
|
+
await screen.findByText(/no files containing/i, { ignore: ".sr-only" });
|
|
214
|
+
expect(status.textContent).toMatch(/no files containing/i);
|
|
182
215
|
});
|
|
183
216
|
|
|
184
217
|
it("renders a truncation notice when a search was capped", async () => {
|
|
@@ -146,6 +146,25 @@ describe("WorkspaceEditor initialPanel", () => {
|
|
|
146
146
|
expect(screen.getByText("new-repo")).toBeTruthy();
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
+
it("labels the manual git inputs for assistive tech (no OAuth connection)", () => {
|
|
150
|
+
// With no gitHubConnection, the panel falls back to manual URL/branch entry.
|
|
151
|
+
render(
|
|
152
|
+
<WorkspaceEditor
|
|
153
|
+
workspace={createMockWorkspace()}
|
|
154
|
+
enableGitHub
|
|
155
|
+
enableLocal={false}
|
|
156
|
+
initialPanel="github"
|
|
157
|
+
/>,
|
|
158
|
+
);
|
|
159
|
+
// Accessible names come from aria-label, not the disappearing placeholder.
|
|
160
|
+
expect(
|
|
161
|
+
screen.getByRole("textbox", { name: "Git repository URL" }),
|
|
162
|
+
).toBeTruthy();
|
|
163
|
+
expect(
|
|
164
|
+
screen.getByRole("textbox", { name: "Branch (optional)" }),
|
|
165
|
+
).toBeTruthy();
|
|
166
|
+
});
|
|
167
|
+
|
|
149
168
|
it("shows loading state when GitHub is checking connection with initialPanel='github'", () => {
|
|
150
169
|
const connection = createMockGitHubConnection({ isLoading: true });
|
|
151
170
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { render, screen, fireEvent, cleanup
|
|
2
|
+
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
|
3
3
|
import { WorkspaceFileSearch } from "../WorkspaceFileSearch";
|
|
4
4
|
import { __clearWorkspaceListingCache } from "../workspaceListingCache";
|
|
5
5
|
import type { WorkspaceEntry } from "../useWorkspaceEntries";
|
|
@@ -74,7 +74,7 @@ describe("WorkspaceFileSearch", () => {
|
|
|
74
74
|
|
|
75
75
|
typeQuery("button");
|
|
76
76
|
const option = await screen.findByRole("option");
|
|
77
|
-
fireEvent.click(
|
|
77
|
+
fireEvent.click(option);
|
|
78
78
|
|
|
79
79
|
expect(onOpenFile).toHaveBeenCalledWith(entry.id, "src/button.tsx");
|
|
80
80
|
// Single entry → no group header.
|
|
@@ -126,9 +126,7 @@ describe("WorkspaceFileSearch", () => {
|
|
|
126
126
|
);
|
|
127
127
|
typeQuery("button");
|
|
128
128
|
const option = await screen.findByRole("option");
|
|
129
|
-
expect(
|
|
130
|
-
"true",
|
|
131
|
-
);
|
|
129
|
+
expect(option.getAttribute("aria-current")).toBe("true");
|
|
132
130
|
});
|
|
133
131
|
|
|
134
132
|
it("shows a no-match state", async () => {
|
|
@@ -140,7 +138,10 @@ describe("WorkspaceFileSearch", () => {
|
|
|
140
138
|
/>,
|
|
141
139
|
);
|
|
142
140
|
typeQuery("zzzzz");
|
|
143
|
-
|
|
141
|
+
// Assert the visible message, not the parallel sr-only live-region copy.
|
|
142
|
+
expect(
|
|
143
|
+
await screen.findByText(/no files matching/i, { ignore: ".sr-only" }),
|
|
144
|
+
).toBeTruthy();
|
|
144
145
|
});
|
|
145
146
|
|
|
146
147
|
it("renders a truncation notice from a notice entry", async () => {
|
|
@@ -184,6 +185,26 @@ describe("WorkspaceFileSearch", () => {
|
|
|
184
185
|
expect(onOpenFile.mock.calls[0][0]).toBe(entry.id);
|
|
185
186
|
});
|
|
186
187
|
|
|
188
|
+
it("announces result status in a polite live region", async () => {
|
|
189
|
+
render(
|
|
190
|
+
<WorkspaceFileSearch
|
|
191
|
+
entries={[makeEntry()]}
|
|
192
|
+
lister={vi.fn(async () => FILES)}
|
|
193
|
+
onOpenFile={vi.fn()}
|
|
194
|
+
/>,
|
|
195
|
+
);
|
|
196
|
+
const status = screen.getByRole("status");
|
|
197
|
+
expect(status.getAttribute("aria-live")).toBe("polite");
|
|
198
|
+
|
|
199
|
+
typeQuery("button");
|
|
200
|
+
await screen.findByRole("option");
|
|
201
|
+
expect(status.textContent).toMatch(/matching file/i);
|
|
202
|
+
|
|
203
|
+
typeQuery("zzzzz");
|
|
204
|
+
await screen.findByText(/no files matching/i, { ignore: ".sr-only" });
|
|
205
|
+
expect(status.textContent).toMatch(/no files matching/i);
|
|
206
|
+
});
|
|
207
|
+
|
|
187
208
|
it("caps rendered results and reports the total", async () => {
|
|
188
209
|
const entry = makeEntry();
|
|
189
210
|
const many: WorkspaceFileEntry[] = Array.from({ length: 250 }, (_, i) => ({
|
|
@@ -152,6 +152,23 @@ describe("WorkspaceSurface", () => {
|
|
|
152
152
|
expect(tab.getAttribute("aria-selected")).toBe("true");
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
+
it("associates the active tab with the single editor tabpanel", () => {
|
|
156
|
+
renderSurface();
|
|
157
|
+
const tab = screen.getByRole("tab", { name: /main\.go/ });
|
|
158
|
+
const panel = screen.getByRole("tabpanel");
|
|
159
|
+
const panelId = panel.getAttribute("id");
|
|
160
|
+
expect(panelId).toBeTruthy();
|
|
161
|
+
expect(tab.getAttribute("id")).toBeTruthy();
|
|
162
|
+
// Tab -> panel and panel -> active tab, both directions wired.
|
|
163
|
+
expect(tab.getAttribute("aria-controls")).toBe(panelId);
|
|
164
|
+
expect(panel.getAttribute("aria-labelledby")).toBe(tab.getAttribute("id"));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("renders no tabpanel when no editor is open (nothing to label)", () => {
|
|
168
|
+
renderSurface({ selectedFile: null, editors: [] });
|
|
169
|
+
expect(screen.queryByRole("tabpanel")).toBeNull();
|
|
170
|
+
});
|
|
171
|
+
|
|
155
172
|
it("collapses back to chat when the toolbar control is clicked", () => {
|
|
156
173
|
const { onCollapse } = renderSurface();
|
|
157
174
|
fireEvent.click(screen.getByRole("button", { name: "Back to chat" }));
|
|
@@ -242,6 +259,59 @@ describe("WorkspaceSurface extraViews", () => {
|
|
|
242
259
|
fireEvent.keyDown(search, { key: "ArrowDown" });
|
|
243
260
|
expect(screen.getByTestId("config-probe")).toBeTruthy();
|
|
244
261
|
});
|
|
262
|
+
|
|
263
|
+
it("reaches an injected view beyond the second by keyboard (focus follows selection)", () => {
|
|
264
|
+
// Regression guard: the old handler moved selection but never DOM focus, so
|
|
265
|
+
// focus stayed pinned on the entered button and every view past the second
|
|
266
|
+
// (Config, Changes, …) was keyboard-unreachable. Each arrow must advance
|
|
267
|
+
// focus so the next arrow computes from the new position.
|
|
268
|
+
renderSurface({ extraViews: [configView, changesView] });
|
|
269
|
+
const explorer = screen.getByRole("radio", { name: "Explorer" });
|
|
270
|
+
explorer.focus();
|
|
271
|
+
expect(document.activeElement).toBe(explorer);
|
|
272
|
+
|
|
273
|
+
// Step 1: Explorer -> Search, focus moves with the selection.
|
|
274
|
+
fireEvent.keyDown(document.activeElement!, { key: "ArrowDown" });
|
|
275
|
+
const search = screen.getByRole("radio", { name: "Search" });
|
|
276
|
+
expect(document.activeElement).toBe(search);
|
|
277
|
+
expect(search.getAttribute("aria-checked")).toBe("true");
|
|
278
|
+
|
|
279
|
+
// Step 2: Search -> Config. The step the old code could not make.
|
|
280
|
+
fireEvent.keyDown(document.activeElement!, { key: "ArrowDown" });
|
|
281
|
+
const config = screen.getByRole("radio", { name: "Config" });
|
|
282
|
+
expect(document.activeElement).toBe(config);
|
|
283
|
+
expect(config.getAttribute("aria-checked")).toBe("true");
|
|
284
|
+
expect(screen.getByTestId("config-probe")).toBeTruthy();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("jumps to the last and first rail views with End and Home", () => {
|
|
288
|
+
renderSurface({ extraViews: [configView, changesView] });
|
|
289
|
+
const explorer = screen.getByRole("radio", { name: "Explorer" });
|
|
290
|
+
explorer.focus();
|
|
291
|
+
|
|
292
|
+
fireEvent.keyDown(document.activeElement!, { key: "End" });
|
|
293
|
+
const changes = screen.getByRole("radio", { name: "Changes (3)" });
|
|
294
|
+
expect(document.activeElement).toBe(changes);
|
|
295
|
+
expect(changes.getAttribute("aria-checked")).toBe("true");
|
|
296
|
+
|
|
297
|
+
fireEvent.keyDown(document.activeElement!, { key: "Home" });
|
|
298
|
+
const explorerAgain = screen.getByRole("radio", { name: "Explorer" });
|
|
299
|
+
expect(document.activeElement).toBe(explorerAgain);
|
|
300
|
+
expect(explorerAgain.getAttribute("aria-checked")).toBe("true");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("moves focus with selection in the Name|Text search-mode toggle", () => {
|
|
304
|
+
renderSurface({ searcher });
|
|
305
|
+
fireEvent.click(screen.getByRole("radio", { name: "Search" }));
|
|
306
|
+
const toggle = screen.getByRole("radiogroup", { name: "Search mode" });
|
|
307
|
+
const name = within(toggle).getByRole("radio", { name: "Name" });
|
|
308
|
+
name.focus();
|
|
309
|
+
|
|
310
|
+
fireEvent.keyDown(document.activeElement!, { key: "ArrowRight" });
|
|
311
|
+
const text = within(toggle).getByRole("radio", { name: "Text" });
|
|
312
|
+
expect(document.activeElement).toBe(text);
|
|
313
|
+
expect(text.getAttribute("aria-checked")).toBe("true");
|
|
314
|
+
});
|
|
245
315
|
});
|
|
246
316
|
|
|
247
317
|
// ---------------------------------------------------------------------------
|