From 1e7456f736027908815f76ff60507a33345b70df Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Thu, 16 Jul 2026 09:59:41 -0600 Subject: [PATCH 1/4] Stabilize input intent audit readiness (#325) --- .github/workflows/ci.yml | 2 +- scripts/e2e/playwright-smoke-args.js | 75 ++++++++++++++++ scripts/e2e/run-playwright-smoke.js | 87 ++++++------------- tests/component/all-tools-discovery.test.tsx | 2 +- .../playwright-smoke-matrix-guard.test.ts | 8 +- tests/unit/playwright-smoke-args.test.ts | 66 ++++++++++++++ 6 files changed, 175 insertions(+), 65 deletions(-) create mode 100644 scripts/e2e/playwright-smoke-args.js create mode 100644 tests/unit/playwright-smoke-args.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4fe19e9..012b2e54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,7 @@ jobs: with: name: route-width-screenshots path: test-results/route-width - if-no-files-found: error + if-no-files-found: warn - name: Run PWA Smoke run: npm run test:e2e:pwa diff --git a/scripts/e2e/playwright-smoke-args.js b/scripts/e2e/playwright-smoke-args.js new file mode 100644 index 00000000..b408a0da --- /dev/null +++ b/scripts/e2e/playwright-smoke-args.js @@ -0,0 +1,75 @@ +const EXCLUSIVE_MODE_FLAGS = new Map([ + ["--first-load-only", "firstLoadOnly"], + ["--input-intents-only", "inputIntentsOnly"], +]); + +export function parsePlaywrightSmokeArgs(argv, { defaultPort = 4173 } = {}) { + const args = { + port: defaultPort, + baseUrl: "", + skipServer: false, + includePwa: false, + firstLoadOnly: false, + writeFirstLoadArtifacts: false, + inputIntentsOnly: false, + }; + const activeExclusiveFlags = new Set(); + const unknownArgs = new Set(); + + for (const arg of argv) { + const exclusiveMode = EXCLUSIVE_MODE_FLAGS.get(arg); + if (exclusiveMode) { + args[exclusiveMode] = true; + activeExclusiveFlags.add(arg); + continue; + } + + if (arg === "--skip-server") { + args.skipServer = true; + continue; + } + + if (arg === "--pwa") { + args.includePwa = true; + continue; + } + + if (arg === "--first-load-artifacts") { + args.writeFirstLoadArtifacts = true; + continue; + } + + if (arg.startsWith("--port=")) { + const parsed = Number(arg.slice("--port=".length)); + if (Number.isFinite(parsed) && parsed > 0) { + args.port = parsed; + } + continue; + } + + if (arg.startsWith("--base-url=")) { + args.baseUrl = arg.slice("--base-url=".length).trim(); + continue; + } + + unknownArgs.add(arg); + } + + const validationErrors = []; + if (unknownArgs.size > 0) { + validationErrors.push(`Unknown argument(s): ${[...unknownArgs].join(", ")}`); + } + if (activeExclusiveFlags.size > 1) { + validationErrors.push(`Conflicting --*-only flags: ${[...activeExclusiveFlags].join(", ")}`); + } + if (validationErrors.length > 0) { + throw new Error(`[playwright-smoke] Invalid arguments:\n- ${validationErrors.join("\n- ")}`); + } + + if (!args.baseUrl) { + args.baseUrl = `http://127.0.0.1:${args.port}`; + } + + args.baseUrl = args.baseUrl.replace(/\/+$/, ""); + return args; +} diff --git a/scripts/e2e/run-playwright-smoke.js b/scripts/e2e/run-playwright-smoke.js index bb0d939f..8a936aea 100644 --- a/scripts/e2e/run-playwright-smoke.js +++ b/scripts/e2e/run-playwright-smoke.js @@ -6,6 +6,7 @@ import process from "node:process"; import axe from "axe-core"; import { chromium } from "playwright"; import serveHandler from "serve-handler"; +import { parsePlaywrightSmokeArgs } from "./playwright-smoke-args.js"; const DEFAULT_PORT = 4173; const DEFAULT_ROUTES = [ @@ -199,64 +200,6 @@ function createRuntimeObserver(page, label, baseUrl = "") { }; } -function parseArgs(argv) { - const args = { - port: DEFAULT_PORT, - baseUrl: "", - skipServer: false, - includePwa: false, - firstLoadOnly: false, - writeFirstLoadArtifacts: false, - inputIntentsOnly: false, - }; - - for (const arg of argv) { - if (arg === "--skip-server") { - args.skipServer = true; - continue; - } - - if (arg === "--pwa") { - args.includePwa = true; - continue; - } - - if (arg === "--first-load-only") { - args.firstLoadOnly = true; - continue; - } - - if (arg === "--first-load-artifacts") { - args.writeFirstLoadArtifacts = true; - continue; - } - - if (arg === "--input-intents-only") { - args.inputIntentsOnly = true; - continue; - } - - if (arg.startsWith("--port=")) { - const parsed = Number(arg.slice("--port=".length)); - if (Number.isFinite(parsed) && parsed > 0) { - args.port = parsed; - } - continue; - } - - if (arg.startsWith("--base-url=")) { - args.baseUrl = arg.slice("--base-url=".length).trim(); - } - } - - if (!args.baseUrl) { - args.baseUrl = `http://127.0.0.1:${args.port}`; - } - - args.baseUrl = args.baseUrl.replace(/\/+$/, ""); - return args; -} - async function wait(ms) { await new Promise((resolve) => setTimeout(resolve, ms)); } @@ -1175,7 +1118,25 @@ async function assertInputIntentSizingMatrix(browser, baseUrl) { try { await page.goto(`${baseUrl}${audit.route}`, { waitUntil: "domcontentloaded" }); await page.waitForSelector("main", { timeout: 15_000 }); - await page.locator("[data-input-intent]").first().waitFor({ state: "attached", timeout: 15_000 }); + await page.waitForFunction((requiredIntents) => { + const visibleIntents = new Set( + Array.from(document.querySelectorAll("[data-input-intent]")) + .filter((element) => { + const style = window.getComputedStyle(element); + const rect = element.getBoundingClientRect(); + return ( + style.display !== "none" && + style.visibility !== "hidden" && + Number(style.opacity) !== 0 && + rect.width > 0 && + rect.height > 0 + ); + }) + .map((element) => element.getAttribute("data-input-intent")) + .filter(Boolean), + ); + return requiredIntents.every((intent) => visibleIntents.has(intent)); + }, audit.requiredIntents, { timeout: 15_000 }); const measurements = await page.evaluate(({ mobile }) => { const isVisible = (element) => { @@ -2201,7 +2162,7 @@ async function main() { port, skipServer, writeFirstLoadArtifacts, - } = parseArgs(process.argv.slice(2)); + } = parsePlaywrightSmokeArgs(process.argv.slice(2), { defaultPort: DEFAULT_PORT }); let serverHandle = null; try { @@ -2247,4 +2208,8 @@ async function main() { } } -main(); +main().catch((error) => { + console.error("[playwright-smoke] FAILED"); + console.error(error instanceof Error ? error.stack || error.message : String(error)); + process.exitCode = 1; +}); diff --git a/tests/component/all-tools-discovery.test.tsx b/tests/component/all-tools-discovery.test.tsx index 0272ceee..fbfe5794 100644 --- a/tests/component/all-tools-discovery.test.tsx +++ b/tests/component/all-tools-discovery.test.tsx @@ -361,7 +361,7 @@ describe("AllToolsDiscovery", () => { expect(screen.getByRole("button", { name: "Show fewer" })).toHaveAttribute("aria-expanded", "true") }) - it("keeps a 300-tool inventory within default and filtered render budgets", () => { + it("keeps a 300-tool inventory within default and filtered render budgets", { timeout: 10_000 }, () => { const { container } = renderLargeInventoryDiscovery() expect(screen.getByRole("status")).toHaveTextContent("300 tools") diff --git a/tests/guards/playwright-smoke-matrix-guard.test.ts b/tests/guards/playwright-smoke-matrix-guard.test.ts index 9e201541..206f3899 100644 --- a/tests/guards/playwright-smoke-matrix-guard.test.ts +++ b/tests/guards/playwright-smoke-matrix-guard.test.ts @@ -3,6 +3,8 @@ import path from "node:path" import { describe, expect, it } from "vitest" const SMOKE_SOURCE = fs.readFileSync(path.join(process.cwd(), "scripts/e2e/run-playwright-smoke.js"), "utf8") +const SMOKE_ARGS_SOURCE = fs.readFileSync(path.join(process.cwd(), "scripts/e2e/playwright-smoke-args.js"), "utf8") +const CI_SOURCE = fs.readFileSync(path.join(process.cwd(), ".github/workflows/ci.yml"), "utf8") describe("playwright smoke matrix guard", () => { it("keeps representative routes and browser journeys in the smoke script", () => { @@ -30,16 +32,18 @@ describe("playwright smoke matrix guard", () => { it("keeps real mobile and desktop input-intent sizing measurements", () => { expect(SMOKE_SOURCE).toContain("INPUT_INTENT_AUDIT_ROUTES") expect(SMOKE_SOURCE).toContain("INPUT_INTENT_AUDIT_VIEWPORTS") - expect(SMOKE_SOURCE).toContain('if (arg === "--input-intents-only")') + expect(SMOKE_ARGS_SOURCE).toContain('["--input-intents-only", "inputIntentsOnly"]') expect(SMOKE_SOURCE).toContain("runInputIntentSmoke") expect(SMOKE_SOURCE).toContain('{ width: 390, height: 844, mobile: true }') expect(SMOKE_SOURCE).toContain('{ width: 1280, height: 900, mobile: false }') expect(SMOKE_SOURCE).toContain('route: "/en/qr-code-generator"') expect(SMOKE_SOURCE).toContain('route: "/en/csv-json-converter"') expect(SMOKE_SOURCE).toContain('document.querySelectorAll("[data-input-intent]")') + expect(SMOKE_SOURCE).toContain("requiredIntents.every((intent) => visibleIntents.has(intent))") expect(SMOKE_SOURCE).toContain("getBoundingClientRect") expect(SMOKE_SOURCE).toContain('page.screenshot({ animations: "disabled", fullPage: false })') expect(SMOKE_SOURCE).toContain("assertNoHorizontalOverflow(page, routeLabel)") + expect(CI_SOURCE).toContain("if-no-files-found: warn") }) it("keeps mobile tool-page regression coverage for core workflows", () => { @@ -66,7 +70,7 @@ describe("playwright smoke matrix guard", () => { }) it("keeps PWA smoke opt-in so CI can cover service worker behavior after export", () => { - expect(SMOKE_SOURCE).toContain('if (arg === "--pwa")') + expect(SMOKE_ARGS_SOURCE).toContain('if (arg === "--pwa")') expect(SMOKE_SOURCE).toContain("assertPwaShellJourney") expect(SMOKE_SOURCE).toContain("serviceWorkers: \"allow\"") expect(SMOKE_SOURCE).toContain("await context.setOffline(true)") diff --git a/tests/unit/playwright-smoke-args.test.ts b/tests/unit/playwright-smoke-args.test.ts new file mode 100644 index 00000000..4b213e00 --- /dev/null +++ b/tests/unit/playwright-smoke-args.test.ts @@ -0,0 +1,66 @@ +import { spawnSync } from "node:child_process" +import path from "node:path" +import { describe, expect, it } from "vitest" +import { parsePlaywrightSmokeArgs } from "../../scripts/e2e/playwright-smoke-args.js" + +const SMOKE_SCRIPT = path.join(process.cwd(), "scripts/e2e/run-playwright-smoke.js") + +describe("playwright smoke CLI arguments", () => { + it.each([ + ["--first-load-only", "firstLoadOnly"], + ["--input-intents-only", "inputIntentsOnly"], + ] as const)("accepts the single exclusive mode %s", (flag, property) => { + const args = parsePlaywrightSmokeArgs([flag]) + + expect(args[property]).toBe(true) + }) + + it("preserves first-load artifact, server, port, and URL combinations", () => { + expect(parsePlaywrightSmokeArgs([ + "--first-load-only", + "--first-load-artifacts", + "--skip-server", + "--pwa", + "--port=4300", + "--base-url=http://localhost:4300/", + ])).toMatchObject({ + baseUrl: "http://localhost:4300", + firstLoadOnly: true, + includePwa: true, + port: 4300, + skipServer: true, + writeFirstLoadArtifacts: true, + }) + }) + + it("rejects and lists conflicting exclusive modes", () => { + const parse = () => parsePlaywrightSmokeArgs(["--first-load-only", "--input-intents-only"]) + + expect(parse).toThrow("Conflicting --*-only flags") + expect(parse).toThrow("--first-load-only, --input-intents-only") + }) + + it("rejects and lists every unknown argument", () => { + const parse = () => parsePlaywrightSmokeArgs(["--mobile-onyl", "unexpected-mode"]) + + expect(parse).toThrow("Unknown argument(s)") + expect(parse).toThrow("--mobile-onyl, unexpected-mode") + }) + + it("exits non-zero before running smoke for invalid CLI arguments", () => { + const result = spawnSync(process.execPath, [ + SMOKE_SCRIPT, + "--first-load-only", + "--input-intents-only", + "--unknown-mode", + ], { + cwd: process.cwd(), + encoding: "utf8", + }) + + expect(result.status).toBe(1) + expect(result.stderr).toContain("Unknown argument(s): --unknown-mode") + expect(result.stderr).toContain("Conflicting --*-only flags: --first-load-only, --input-intents-only") + expect(result.stdout).not.toContain("PASS") + }) +}) From 5a72cc72dd57571d8118ffd4a435265e0c7f67fd Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Thu, 16 Jul 2026 10:22:42 -0600 Subject: [PATCH 2/4] Fix accessible dialog focus workflows (#263) --- src/components/layout/command-palette.tsx | 14 +- .../layout/deferred-command-palette.tsx | 21 +- src/components/ui/command.tsx | 4 + .../tools/json-formatter/components.tsx | 6 + src/features/tools/json-formatter/page.tsx | 73 +++--- src/features/tools/json-formatter/panels.tsx | 9 +- .../json-formatter/use-json-tree-dialog.ts | 51 ++++ src/features/tools/pipeline-builder/page.tsx | 58 +++-- .../pipeline-privacy-preview.tsx | 91 ++++--- src/generated/route-width-inventory.json | 5 +- src/hooks/use-dialog-return-focus.ts | 47 ++++ .../controlled-dialog-focus.test.tsx | 237 ++++++++++++++++++ tests/component/layout-components.test.tsx | 8 +- .../phase3-pipeline-builder-page.test.tsx | 59 ++++- 14 files changed, 578 insertions(+), 105 deletions(-) create mode 100644 src/features/tools/json-formatter/use-json-tree-dialog.ts create mode 100644 src/hooks/use-dialog-return-focus.ts create mode 100644 tests/component/controlled-dialog-focus.test.tsx diff --git a/src/components/layout/command-palette.tsx b/src/components/layout/command-palette.tsx index 561b00bb..f186836d 100644 --- a/src/components/layout/command-palette.tsx +++ b/src/components/layout/command-palette.tsx @@ -21,6 +21,7 @@ import { getCommandSearchToolByKey } from "@/generated/command-search-index" import { readFavoriteToolKeys, readRecentToolKeys, TOOL_DISCOVERY_UPDATED_EVENT } from "@/core/storage/tool-discovery-state" import { useSystemCommands } from "@/core/commands/registry" import { applyToolSearchScoreBonuses, scoreCommandSearch } from "@/core/search/command-search" +import { useDialogReturnFocus } from "@/hooks/use-dialog-return-focus" import { getToolSearchMetadata, getToolSearchMetadataTerms, @@ -91,6 +92,7 @@ type CommandPaletteProps = { open?: boolean onOpenChange?: (open: boolean) => void enableShortcut?: boolean + takeReturnFocusTarget?: () => HTMLElement | null } function isEditableShortcutTarget(target: EventTarget | null): boolean { @@ -204,11 +206,17 @@ function ToolCommandRow({ ) } -export function CommandPalette({ open: openProp, onOpenChange, enableShortcut = true }: CommandPaletteProps = {}) { +export function CommandPalette({ + open: openProp, + onOpenChange, + enableShortcut = true, + takeReturnFocusTarget, +}: CommandPaletteProps = {}) { const [internalOpen, setInternalOpen] = React.useState(false) const [favoriteToolKeys, setFavoriteToolKeys] = React.useState([]) const [recentToolKeys, setRecentToolKeys] = React.useState([]) const router = useRouter() + const { captureReturnFocus, restoreReturnFocus } = useDialogReturnFocus(takeReturnFocusTarget) const { lang, t, englishToolSearchAliases } = useLang() const commonLabels = t.common const navigationLabel = requireTranslationValue(t.nav.navigation, "nav.navigation") @@ -248,13 +256,14 @@ export function CommandPalette({ open: openProp, onOpenChange, enableShortcut = if (e.defaultPrevented || isEditableShortcutTarget(e.target)) return if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault() + captureReturnFocus() setOpen((prev) => !prev) } } document.addEventListener("keydown", down) return () => document.removeEventListener("keydown", down) - }, [enableShortcut, setOpen]) + }, [captureReturnFocus, enableShortcut, setOpen]) React.useEffect(() => { const sync = () => { @@ -379,6 +388,7 @@ export function CommandPalette({ open: openProp, onOpenChange, enableShortcut = { diff --git a/src/components/layout/deferred-command-palette.tsx b/src/components/layout/deferred-command-palette.tsx index bfa4b5d4..3c2028bb 100644 --- a/src/components/layout/deferred-command-palette.tsx +++ b/src/components/layout/deferred-command-palette.tsx @@ -25,9 +25,17 @@ function isEditableShortcutTarget(target: EventTarget | null): boolean { export function DeferredCommandPalette() { const [isMounted, setIsMounted] = React.useState(false) const [open, setOpen] = React.useState(false) + const returnFocusRef = React.useRef(null) + const takeReturnFocusTarget = React.useCallback(() => { + const focusTarget = returnFocusRef.current + returnFocusRef.current = null + return focusTarget + }, []) React.useEffect(() => { - const activatePalette = () => { + const activatePalette = (focusTarget?: HTMLElement | null) => { + const activeElement = focusTarget ?? document.activeElement + returnFocusRef.current = activeElement instanceof HTMLElement ? activeElement : null setIsMounted(true) setOpen(true) } @@ -46,7 +54,7 @@ export function DeferredCommandPalette() { if (!trigger) return e.preventDefault() - activatePalette() + activatePalette(trigger instanceof HTMLElement ? trigger : null) } document.addEventListener("keydown", down) @@ -59,5 +67,12 @@ export function DeferredCommandPalette() { if (!isMounted) return null - return + return ( + + ) } diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx index a6dfb0dc..ed94a170 100644 --- a/src/components/ui/command.tsx +++ b/src/components/ui/command.tsx @@ -36,6 +36,7 @@ function CommandDialog({ className, showCloseButton = true, filter, + onCloseAutoFocus, ...props }: React.ComponentProps & { title?: string @@ -43,12 +44,14 @@ function CommandDialog({ className?: string showCloseButton?: boolean filter?: React.ComponentProps["filter"] + onCloseAutoFocus?: React.ComponentProps["onCloseAutoFocus"] }) { return ( {title} @@ -57,6 +60,7 @@ function CommandDialog({ {children} diff --git a/src/features/tools/json-formatter/components.tsx b/src/features/tools/json-formatter/components.tsx index ab58959a..47adbff8 100644 --- a/src/features/tools/json-formatter/components.tsx +++ b/src/features/tools/json-formatter/components.tsx @@ -83,6 +83,8 @@ export function JsonTreeNode({ variant="ghost" size="icon" className="h-6 w-6" + data-json-tree-action="rename-key" + data-json-tree-path={JSON.stringify(path)} onClick={() => handleRenameKey(parentPath, keyName)} > @@ -94,6 +96,8 @@ export function JsonTreeNode({ variant="ghost" size="icon" className="h-6 w-6" + data-json-tree-action="add-child" + data-json-tree-path={JSON.stringify(path)} onClick={() => handleAddChild(path)} > @@ -104,6 +108,8 @@ export function JsonTreeNode({ variant="ghost" size="icon" className="h-6 w-6" + data-json-tree-action="edit-node" + data-json-tree-path={JSON.stringify(path)} onClick={() => handleEditNode(path, value)} > diff --git a/src/features/tools/json-formatter/page.tsx b/src/features/tools/json-formatter/page.tsx index d8014d2b..e6e5b496 100644 --- a/src/features/tools/json-formatter/page.tsx +++ b/src/features/tools/json-formatter/page.tsx @@ -49,8 +49,10 @@ import { import { JsonTreeNode } from "./components" import { SAMPLE_JSON_SOURCE } from "./samples" import { downloadJsonOutput } from "./browser-actions" -import type { JsonPath, JsonValue, TreeDialogState, ViewMode } from "./types" +import type { JsonPath, JsonValue, ViewMode } from "./types" import { WideToolPageContainer } from "@/components/layout/page-container" +import { useJsonTreeDialog } from "./use-json-tree-dialog" + export function JsonFormatterPage() { const { t, lang } = useLang() const toolT = t.tools["json_formatter"] as Record @@ -64,10 +66,17 @@ export function JsonFormatterPage() { const [outputWrapMode, setOutputWrapMode] = React.useState("wrap") const [treeData, setTreeData] = React.useState(null) const [expanded, setExpanded] = React.useState>(new Set(["$"])) - const [treeDialog, setTreeDialog] = React.useState(null) const [searchQuery, setSearchQuery] = React.useState("") const [isSearchOpen, setIsSearchOpen] = React.useState(false) const [isFormatting, setIsFormatting] = React.useState(false) + const { + closeTreeDialog, + openTreeDialog, + prepareRenameReturnFocus, + restoreTreeDialogReturnFocus, + treeDialog, + updateTreeDialogDraft, + } = useJsonTreeDialog() const searchResults = React.useMemo(() => { if (!treeData || !searchQuery.trim()) return { matched: new Set(), parents: new Set() } @@ -454,11 +463,11 @@ export function JsonFormatterPage() { }) } - const handleEditNode = (path: JsonPath, currentValue: JsonValue) => { - setTreeDialog({ - type: "edit_value", - path, - draft: JSON.stringify(currentValue), + const handleEditNode = (path: JsonPath, currentValue: JsonValue) => { + openTreeDialog({ + type: "edit_value", + path, + draft: JSON.stringify(currentValue), }) } @@ -480,38 +489,24 @@ export function JsonFormatterPage() { } if (isJsonObject(node)) { - setTreeDialog({ - type: "add_key", - path, - draft: "", + openTreeDialog({ + type: "add_key", + path, + draft: "", }) } } - - const handleRenameKey = (parentPath: JsonPath, currentKey: string) => { - setTreeDialog({ - type: "rename_key", - parentPath, - currentKey, + + const handleRenameKey = (parentPath: JsonPath, currentKey: string) => { + openTreeDialog({ + type: "rename_key", + parentPath, + currentKey, draft: currentKey, }) } - const closeTreeDialog = () => { - setTreeDialog(null) - } - - const updateTreeDialogDraft = (draft: string) => { - setTreeDialog((prev) => { - if (!prev) return prev - return { - ...prev, - draft, - } - }) - } - - const confirmTreeDialog = () => { + const confirmTreeDialog = () => { if (treeData === null || treeDialog === null) { closeTreeDialog() return @@ -555,12 +550,13 @@ export function JsonFormatterPage() { } const nextTree = renameObjectKey(treeData, treeDialog.parentPath, treeDialog.currentKey, nextKey) - if (nextTree === treeData) { - toast.error(text("rename_key_failed")) - return - } - applyTreeValue(nextTree) - closeTreeDialog() + if (nextTree === treeData) { + toast.error(text("rename_key_failed")) + return + } + prepareRenameReturnFocus(treeDialog.parentPath, nextKey) + applyTreeValue(nextTree) + closeTreeDialog() } return ( @@ -570,6 +566,7 @@ export function JsonFormatterPage() { closeLabel={t.common.close} dialog={treeDialog} onClose={closeTreeDialog} + onCloseAutoFocus={restoreTreeDialogReturnFocus} onDraftChange={updateTreeDialogDraft} onSubmit={confirmTreeDialog} text={text} diff --git a/src/features/tools/json-formatter/panels.tsx b/src/features/tools/json-formatter/panels.tsx index 09880ce7..40927ebd 100644 --- a/src/features/tools/json-formatter/panels.tsx +++ b/src/features/tools/json-formatter/panels.tsx @@ -72,6 +72,7 @@ interface JsonTreeEditDialogProps { closeLabel: string dialog: TreeDialogState onClose: () => void + onCloseAutoFocus: (event: Event) => void onDraftChange: (draft: string) => void onSubmit: () => void text: (key: string) => string @@ -82,6 +83,7 @@ export function JsonTreeEditDialog({ closeLabel, dialog, onClose, + onCloseAutoFocus, onDraftChange, onSubmit, text, @@ -101,7 +103,10 @@ export function JsonTreeEditDialog({ return ( { if (!open) onClose() }}> - + {title} {description} @@ -116,6 +121,7 @@ export function JsonTreeEditDialog({ {dialog?.type === "edit_value" ? (