@marimo-team/frontend 0.25.3-dev129 → 0.25.3-dev134
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/dist/assets/{CellStatus-6xDEj3Hs.js → CellStatus-DC6FFQOg.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-DCcLAktu.js → ConnectedDataExplorerComponent-BV1mVpaE.js} +1 -1
- package/dist/assets/{ErrorBoundary-98Xo04DS.js → ErrorBoundary-Ce-Ujlwj.js} +2 -2
- package/dist/assets/{JsonOutput-V47aiTIK.js → JsonOutput-CfEBzaqW.js} +4 -4
- package/dist/assets/{MarimoErrorOutput-DeWZNBgL.js → MarimoErrorOutput-Bh6o9zle.js} +1 -1
- package/dist/assets/{RenderHTML-Bjvcs70-.js → RenderHTML-COGZsXQV.js} +1 -1
- package/dist/assets/{RunButton-CnvvXD9d.js → RunButton-DQ51plUv.js} +1 -1
- package/dist/assets/{add-cell-with-ai-BbuqzITi.js → add-cell-with-ai-D7gtGUsg.js} +1 -1
- package/dist/assets/{add-connection-dialog-DIC7owvZ.js → add-connection-dialog-CpMCc97S.js} +1 -1
- package/dist/assets/{agent-panel-D8hO2P1x.js → agent-panel-BSFYqJ9B.js} +2 -2
- package/dist/assets/{ai-model-dropdown-DgmiG_5j.js → ai-model-dropdown-D575Vid8.js} +3 -3
- package/dist/assets/{app-config-button-UZHv1Iim.js → app-config-button-Cfcg_5Bz.js} +1 -1
- package/dist/assets/{blob-BPknzzkY.js → blob-CszMu60N.js} +1 -1
- package/dist/assets/{cell-editor-CM-K1_wq.js → cell-editor-BLpOlLnM.js} +2 -2
- package/dist/assets/cell-link-Cj0gGaMV.js +1 -0
- package/dist/assets/{cells-BypTg035.js → cells-SvbLY10y.js} +1 -1
- package/dist/assets/{chat-display-BRLNOoTF.js → chat-display-D43_g3TX.js} +1 -1
- package/dist/assets/{chat-panel-D02Rk9we.js → chat-panel-B_HAJMwx.js} +1 -1
- package/dist/assets/{chat-ui-DFN_RwxF.js → chat-ui-BEcYCphu.js} +2 -2
- package/dist/assets/{column-preview-85ORMhfB.js → column-preview-CrzaJKk8.js} +1 -1
- package/dist/assets/{command-palette-CtyjjdsA.js → command-palette-Bm9yIVcD.js} +1 -1
- package/dist/assets/{common-J265II7i.js → common-BJyUxCzv.js} +1 -1
- package/dist/assets/{components-Bd3Dpi92.js → components-D1f_Cz4i.js} +1 -1
- package/dist/assets/{datasource-Dy4rzzSP.js → datasource-C8KSVfSD.js} +1 -1
- package/dist/assets/{dependency-graph-panel-ByGFFT4E.js → dependency-graph-panel-BhmyJbUl.js} +1 -1
- package/dist/assets/{documentation-panel-LxuJCY3M.js → documentation-panel-DcVG0Q0d.js} +1 -1
- package/dist/assets/{download-BsBJWH_j.js → download-Bx52VRE4.js} +1 -1
- package/dist/assets/{edit-page--d6X-OO9.js → edit-page-DUrwOHR-.js} +6 -6
- package/dist/assets/{error-entries-0wTSSuK9.js → error-entries-11OuHzUF.js} +1 -1
- package/dist/assets/{error-panel-ZNdbE3Fo.js → error-panel-XJy5S25F.js} +1 -1
- package/dist/assets/{feedback-button-C6asPmsh.js → feedback-button-B7jh_B4w.js} +1 -1
- package/dist/assets/feedback-button-C-eKtxXE.js +1 -0
- package/dist/assets/{file-explorer-panel-0fUP_ptL.js → file-explorer-panel-CnVxbF4A.js} +1 -1
- package/dist/assets/{file-name-input-B8lzI5iS.js → file-name-input-BJuqjQqH.js} +2 -2
- package/dist/assets/floating-outline-8yyZ-MOJ.js +1 -0
- package/dist/assets/{gallery-page-ClR8U-km.js → gallery-page-DJvGHePf.js} +1 -1
- package/dist/assets/{glide-data-editor-B6QQBQwS.js → glide-data-editor-Cyc8DHZx.js} +1 -1
- package/dist/assets/{home-page-CJnqDcT9.js → home-page-tRZJmSU6.js} +1 -1
- package/dist/assets/{hooks-CCek4i9f.js → hooks-CxGwybGw.js} +1 -1
- package/dist/assets/index-CoxbqhGH.js +30 -0
- package/dist/assets/index-u08m0hQE.css +2 -0
- package/dist/assets/{layout-66Urh4Zg.js → layout-DvXJxBiL.js} +5 -5
- package/dist/assets/{logs-panel-DFhkIcge.js → logs-panel-KtsY7TrR.js} +1 -1
- package/dist/assets/{markdown-renderer-mYYQYD2m.js → markdown-renderer-DUgV7_fm.js} +1 -1
- package/dist/assets/{name-cell-input-Crcu3i2z.js → name-cell-input-C3-Cj-Ek.js} +1 -1
- package/dist/assets/{outline-panel-BFIPK0H_.js → outline-panel-BzyY4JA6.js} +1 -1
- package/dist/assets/{packages-panel-B4KtjgkF.js → packages-panel-DxosbWko.js} +1 -1
- package/dist/assets/{panel-accordion-state-D6KtwSF8.js → panel-accordion-state-BRbTib2B.js} +1 -1
- package/dist/assets/panels-Da3071Q4.js +1 -0
- package/dist/assets/{process-output-DDvMd6Jh.js → process-output-CSwx7rvo.js} +1 -1
- package/dist/assets/{readonly-code-display-VDqYn2d0.js → readonly-code-display-B9y9423Y.js} +1 -1
- package/dist/assets/{readonly-python-code-CZUKLXJ4.js → readonly-python-code-Dgw6trlk.js} +1 -1
- package/dist/assets/{reveal-component-BMmDU1uN.js → reveal-component-B9A4wkG-.js} +1 -1
- package/dist/assets/{run-page-BmOPPh2Y.js → run-page-C1MQ5b_A.js} +1 -1
- package/dist/assets/{runtime-details-state-CipqynUD.js → runtime-details-state-D_hOQL9T.js} +1 -1
- package/dist/assets/scratchpad-panel-CBCsPmMW.js +1 -0
- package/dist/assets/secrets-panel-ConQpchl.js +1 -0
- package/dist/assets/session-panel-CiHCKPlx.js +1 -0
- package/dist/assets/snippets-panel-C0VDE_4i.js +1 -0
- package/dist/assets/{state-QfGh3m2G.js → state-DrraMmqz.js} +1 -1
- package/dist/assets/{state-BLGvzMB6.js → state-DvU2AiFx.js} +22 -22
- package/dist/assets/tracing-C5d51Nmi.js +1 -0
- package/dist/assets/{tracing-panel-CsHuzRGP.js → tracing-panel-B89R8dWE.js} +2 -2
- package/dist/assets/{tree-actions-BQw_utcj.js → tree-actions-Cph8FVzh.js} +1 -1
- package/dist/assets/{useAddCell-_b8InOZC.js → useAddCell-VPkB72pn.js} +1 -1
- package/dist/assets/{useCellActionButton-BbgMzBdk.js → useCellActionButton--zI3GFnt.js} +1 -1
- package/dist/assets/{useDeleteCell-DhoFbEkU.js → useDeleteCell-CjewKeSr.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-COTULhoH.js → useDependencyPanelTab-Bt_ka-wl.js} +1 -1
- package/dist/assets/{useNotebookActions-xnA_l3nY.js → useNotebookActions-DV2S6KaG.js} +1 -1
- package/dist/assets/{useRunCells-VBCe6CLn.js → useRunCells-BupCZpzk.js} +1 -1
- package/dist/assets/{useSplitCell-AvSpVAmq.js → useSplitCell-CaLIR0TS.js} +1 -1
- package/dist/index.html +21 -21
- package/package.json +1 -1
- package/src/__tests__/module-entry.test.ts +32 -0
- package/src/components/ai/__tests__/ai-model-dropdown.test.tsx +63 -0
- package/src/components/ai/ai-model-dropdown.tsx +11 -6
- package/src/components/chat/acp/__tests__/session-tabs.test.tsx +43 -0
- package/src/components/chat/acp/blocks.tsx +1 -0
- package/src/components/chat/acp/session-tabs.tsx +8 -3
- package/src/components/data-table/TableTopBar.tsx +2 -0
- package/src/components/data-table/__tests__/data-table.test.tsx +1 -0
- package/src/components/data-table/__tests__/keyboard-selection.test.tsx +188 -0
- package/src/components/data-table/data-table.tsx +6 -0
- package/src/components/data-table/range-focus/atoms.ts +4 -6
- package/src/components/data-table/range-focus/use-cell-range-selection.ts +65 -38
- package/src/components/data-table/range-focus/use-scroll-into-view.ts +21 -2
- package/src/components/data-table/renderers.tsx +16 -7
- package/src/components/data-table/row-viewer-panel/__tests__/row-viewer.test.tsx +24 -1
- package/src/components/data-table/row-viewer-panel/row-viewer.tsx +2 -2
- package/src/components/datasources/datasources.tsx +1 -0
- package/src/components/editor/__tests__/package-alert.test.tsx +22 -1
- package/src/components/editor/cell/collapse.tsx +5 -3
- package/src/components/editor/chrome/panels/outline/__tests__/outline-keyboard.test.tsx +84 -0
- package/src/components/editor/chrome/panels/outline/floating-outline.tsx +39 -14
- package/src/components/editor/chrome/panels/outline/useActiveOutline.tsx +3 -2
- package/src/components/editor/chrome/panels/secrets-panel.tsx +1 -0
- package/src/components/editor/chrome/panels/snippets-panel.tsx +5 -1
- package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +4 -4
- package/src/components/editor/controls/__tests__/keyboard-shortcuts.test.tsx +61 -0
- package/src/components/editor/controls/keyboard-shortcuts.tsx +14 -5
- package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +4 -4
- package/src/components/editor/file-tree/__tests__/file-explorer-navigation.test.tsx +47 -0
- package/src/components/editor/file-tree/file-explorer.tsx +35 -3
- package/src/components/editor/file-tree/file-name-input.tsx +1 -0
- package/src/components/editor/header/status.tsx +28 -5
- package/src/components/editor/links/__tests__/cell-link.test.tsx +146 -0
- package/src/components/editor/links/cell-link.tsx +14 -5
- package/src/components/editor/navigation/focus-utils.ts +4 -4
- package/src/components/editor/notebook-cell.tsx +11 -2
- package/src/components/editor/output/MarimoTracebackOutput.tsx +17 -15
- package/src/components/editor/output/__tests__/traceback.test.tsx +27 -1
- package/src/components/editor/package-alert.tsx +8 -3
- package/src/components/scratchpad/scratchpad.test.tsx +95 -0
- package/src/components/scratchpad/scratchpad.tsx +14 -1
- package/src/components/tracing/tracing-keyboard.test.tsx +85 -0
- package/src/components/tracing/tracing.tsx +43 -39
- package/src/components/ui/__tests__/draggable-popover.test.tsx +24 -0
- package/src/components/ui/draggable-popover.tsx +24 -2
- package/src/core/attachments/state.ts +2 -2
- package/src/core/document/__tests__/codemirror.test.ts +3 -1
- package/src/core/document/__tests__/hooks.test.tsx +7 -7
- package/src/core/document/__tests__/model.test.ts +44 -11
- package/src/core/document/__tests__/scenario-fixture.ts +2 -1
- package/src/core/document/__tests__/test-notebook.ts +19 -9
- package/src/core/document/draft.ts +1 -1
- package/src/core/document/index.ts +3 -10
- package/src/core/document/model.ts +14 -11
- package/src/core/document/state.ts +1 -1
- package/src/core/document/sync.ts +24 -18
- package/src/core/host/AGENTS.md +3 -7
- package/src/core/host/__tests__/fixtures.ts +30 -0
- package/src/core/host/__tests__/{notebook-client.test.ts → replica.test.ts} +42 -74
- package/src/core/host/__tests__/wasm.test.ts +8 -0
- package/src/core/host/backend.ts +4 -0
- package/src/core/host/index.ts +14 -0
- package/src/core/host/{notebook-client.ts → replica.ts} +75 -86
- package/src/core/host/static.ts +2 -0
- package/src/core/host/wasm.ts +10 -1
- package/src/core/wasm/bridge.ts +1 -1
- package/src/core/websocket/__tests__/backend.test.ts +111 -22
- package/src/core/websocket/__tests__/useNotebookReplica.hook.test.tsx +141 -0
- package/src/core/websocket/__tests__/useNotebookSession.hook.test.tsx +4 -4
- package/src/core/websocket/backend.ts +43 -5
- package/src/core/websocket/connection-state.ts +1 -1
- package/src/core/websocket/state.ts +3 -3
- package/src/core/websocket/transports/__tests__/notebook.test.ts +11 -21
- package/src/core/websocket/types.ts +2 -2
- package/src/core/websocket/useNotebookReplica.ts +69 -0
- package/src/core/websocket/useNotebookSession.tsx +6 -6
- package/src/plugins/impl/chat/chat-ui.tsx +2 -1
- package/src/plugins/impl/data-explorer/components/column-summary.tsx +4 -2
- package/src/plugins/impl/data-frames/__tests__/sidebar.test.tsx +160 -0
- package/src/plugins/impl/data-frames/panel.tsx +43 -16
- package/dist/assets/cell-link-CAjuFcLK.js +0 -1
- package/dist/assets/feedback-button-Bt89tGG2.js +0 -1
- package/dist/assets/floating-outline-CRpmOv32.js +0 -1
- package/dist/assets/index-B5pWkt7a.js +0 -30
- package/dist/assets/index-BnhSnwvB.css +0 -2
- package/dist/assets/panels-DiwGKPzK.js +0 -1
- package/dist/assets/scratchpad-panel-Drr_kuwb.js +0 -1
- package/dist/assets/secrets-panel-D-W1PL76.js +0 -1
- package/dist/assets/session-panel-W7JOcog_.js +0 -1
- package/dist/assets/snippets-panel-cUPEdm66.js +0 -1
- package/dist/assets/tracing-BNFynaTu.js +0 -1
- package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +0 -228
- package/src/core/websocket/useNotebookClient.ts +0 -89
|
@@ -276,13 +276,18 @@ const ProviderDropdownContent = ({
|
|
|
276
276
|
const qualifiedModelId: QualifiedModelId = `${provider}/${model.model}`;
|
|
277
277
|
return (
|
|
278
278
|
<DropdownMenuSub key={qualifiedModelId}>
|
|
279
|
-
<DropdownMenuSubTrigger
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
279
|
+
<DropdownMenuSubTrigger
|
|
280
|
+
showChevron={false}
|
|
281
|
+
className="py-2"
|
|
282
|
+
onClick={() => onSelect(qualifiedModelId)}
|
|
283
|
+
onKeyDown={(e) => {
|
|
284
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
285
|
+
e.preventDefault();
|
|
283
286
|
onSelect(qualifiedModelId);
|
|
284
|
-
}
|
|
285
|
-
|
|
287
|
+
}
|
|
288
|
+
}}
|
|
289
|
+
>
|
|
290
|
+
<div className="flex items-center gap-2 w-full cursor-pointer">
|
|
286
291
|
<AiModelDropdownItem model={model} provider={provider} />
|
|
287
292
|
</div>
|
|
288
293
|
</DropdownMenuSubTrigger>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
|
+
import { createStore, Provider } from "jotai";
|
|
5
|
+
import { expect, it, vi } from "vitest";
|
|
6
|
+
import { SessionTabs } from "../session-tabs";
|
|
7
|
+
import { addSession, agentSessionStateAtom } from "../state";
|
|
8
|
+
|
|
9
|
+
vi.mock("../agent-selector", () => ({ AgentSelector: () => null }));
|
|
10
|
+
|
|
11
|
+
it("keeps session selection and close as separate named buttons", () => {
|
|
12
|
+
const first = addSession(
|
|
13
|
+
{ sessions: [], activeTabId: null },
|
|
14
|
+
{ agentId: "claude", firstMessage: "First" },
|
|
15
|
+
).sessions[0];
|
|
16
|
+
const second = addSession(
|
|
17
|
+
{ sessions: [], activeTabId: null },
|
|
18
|
+
{ agentId: "codex", firstMessage: "Second" },
|
|
19
|
+
).sessions[0];
|
|
20
|
+
const store = createStore();
|
|
21
|
+
store.set(agentSessionStateAtom, {
|
|
22
|
+
sessions: [first, second],
|
|
23
|
+
activeTabId: first.tabId,
|
|
24
|
+
});
|
|
25
|
+
render(
|
|
26
|
+
<Provider store={store}>
|
|
27
|
+
<SessionTabs />
|
|
28
|
+
</Provider>,
|
|
29
|
+
);
|
|
30
|
+
const select = screen.getByRole("button", {
|
|
31
|
+
name: /Second$/,
|
|
32
|
+
pressed: false,
|
|
33
|
+
});
|
|
34
|
+
select.focus();
|
|
35
|
+
expect(select).toHaveFocus();
|
|
36
|
+
fireEvent.click(select);
|
|
37
|
+
expect(select).toHaveAttribute("aria-pressed", "true");
|
|
38
|
+
fireEvent.click(screen.getByRole("button", { name: "Close First" }));
|
|
39
|
+
expect(store.get(agentSessionStateAtom).activeTabId).toBe(second.tabId);
|
|
40
|
+
expect(
|
|
41
|
+
store.get(agentSessionStateAtom).sessions.map((session) => session.title),
|
|
42
|
+
).toEqual(["Second"]);
|
|
43
|
+
});
|
|
@@ -337,6 +337,7 @@ export const PlansBlock = (props: { data: PlanNotificationEvent[] }) => {
|
|
|
337
337
|
>
|
|
338
338
|
<input
|
|
339
339
|
type="checkbox"
|
|
340
|
+
aria-label={item.content}
|
|
340
341
|
checked={item.status === "completed"}
|
|
341
342
|
readOnly={true}
|
|
342
343
|
className="accent-primary h-4 w-4 rounded border border-muted-foreground/30"
|
|
@@ -32,17 +32,22 @@ const SessionTab: React.FC<SessionTabProps> = memo(
|
|
|
32
32
|
"flex items-center gap-1 px-2 py-1 text-xs border-r border-border bg-muted/30 hover:bg-muted/50 cursor-pointer min-w-0",
|
|
33
33
|
isActive && "bg-background border-b-0 relative z-1",
|
|
34
34
|
)}
|
|
35
|
-
onClick={() => onSelect(session.tabId)}
|
|
36
35
|
>
|
|
37
|
-
<
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
aria-pressed={isActive}
|
|
39
|
+
onClick={() => onSelect(session.tabId)}
|
|
40
|
+
className="flex items-center gap-1 min-w-0 flex-1 text-left focus-visible:outline focus-visible:outline-ring"
|
|
41
|
+
>
|
|
38
42
|
<span className="text-muted-foreground text-[10px] font-medium">
|
|
39
43
|
<AiProviderIcon provider={session.agentId} className="h-3 w-3" />
|
|
40
44
|
</span>
|
|
41
45
|
<span className="truncate" title={session.title}>
|
|
42
46
|
{session.title}
|
|
43
47
|
</span>
|
|
44
|
-
</
|
|
48
|
+
</button>
|
|
45
49
|
<Button
|
|
50
|
+
aria-label={`Close ${session.title}`}
|
|
46
51
|
variant="ghost"
|
|
47
52
|
size="sm"
|
|
48
53
|
className="h-4 w-4 p-0 hover:bg-destructive/20 hover:text-destructive shrink-0"
|
|
@@ -76,6 +76,7 @@ export const TableTopBar = <TData,>({
|
|
|
76
76
|
<input
|
|
77
77
|
ref={inputRef}
|
|
78
78
|
type="text"
|
|
79
|
+
aria-label="Search table"
|
|
79
80
|
className="h-6 border-none bg-transparent focus:outline-hidden text-sm w-full min-w-0"
|
|
80
81
|
value={internalValue}
|
|
81
82
|
onKeyDown={(e) => {
|
|
@@ -90,6 +91,7 @@ export const TableTopBar = <TData,>({
|
|
|
90
91
|
{reloading && <Spinner size="small" />}
|
|
91
92
|
{internalValue && (
|
|
92
93
|
<Button
|
|
94
|
+
aria-label="Clear table search"
|
|
93
95
|
variant="text"
|
|
94
96
|
size="xs"
|
|
95
97
|
className="h-5 w-5 p-0 shrink-0"
|
|
@@ -260,6 +260,7 @@ describe("DataTable", () => {
|
|
|
260
260
|
);
|
|
261
261
|
|
|
262
262
|
const cells = within(screen.getAllByRole("row")[1]).getAllByRole("cell");
|
|
263
|
+
cells[1].scrollIntoView = vi.fn();
|
|
263
264
|
// Start a pending hover-show on cell A, then focus cell B before it fires.
|
|
264
265
|
fireEvent.mouseOver(cells[0], { buttons: 0 });
|
|
265
266
|
fireEvent.focus(cells[1]);
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
7
|
+
import { copyToClipboard } from "@/utils/copy";
|
|
8
|
+
import type { CellSelectionState } from "../cell-selection/types";
|
|
9
|
+
import { DataTable } from "../data-table";
|
|
10
|
+
|
|
11
|
+
vi.mock("@/utils/copy", () => ({ copyToClipboard: vi.fn() }));
|
|
12
|
+
|
|
13
|
+
function SelectableTable({ multiple = false }: { multiple?: boolean }) {
|
|
14
|
+
const [selection, setSelection] = useState<CellSelectionState>([]);
|
|
15
|
+
return (
|
|
16
|
+
<TooltipProvider>
|
|
17
|
+
<DataTable
|
|
18
|
+
data={[{ name: "Alice" }, { name: "Bob" }]}
|
|
19
|
+
columns={[{ accessorKey: "name", header: "Name" }]}
|
|
20
|
+
totalRows={2}
|
|
21
|
+
totalColumns={1}
|
|
22
|
+
pagination={false}
|
|
23
|
+
selection={multiple ? "multi-cell" : "single-cell"}
|
|
24
|
+
cellSelection={selection}
|
|
25
|
+
onCellSelectionChange={setSelection}
|
|
26
|
+
/>
|
|
27
|
+
</TooltipProvider>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("table keyboard access", () => {
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.useRealTimers();
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("includes the initial focused cell when extending a keyboard range", () => {
|
|
38
|
+
render(<SelectableTable />);
|
|
39
|
+
const [alice, bob] = screen.getAllByRole("gridcell");
|
|
40
|
+
act(() => alice.focus());
|
|
41
|
+
fireEvent.keyDown(alice, { key: "ArrowDown", shiftKey: true });
|
|
42
|
+
fireEvent.keyDown(bob, { key: "c", ctrlKey: true });
|
|
43
|
+
expect(copyToClipboard).toHaveBeenLastCalledWith("Alice\nBob", undefined);
|
|
44
|
+
fireEvent.keyDown(bob, { key: "ArrowUp", shiftKey: true });
|
|
45
|
+
fireEvent.keyDown(alice, { key: "c", ctrlKey: true });
|
|
46
|
+
expect(copyToClipboard).toHaveBeenLastCalledWith("Alice", undefined);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it.each(["Enter", " "])(
|
|
50
|
+
"copies the activated cell with %s, including after an older selection",
|
|
51
|
+
(key) => {
|
|
52
|
+
render(<SelectableTable />);
|
|
53
|
+
const [alice, bob] = screen.getAllByRole("gridcell");
|
|
54
|
+
for (const [cell, value] of [
|
|
55
|
+
[alice, "Alice"],
|
|
56
|
+
[bob, "Bob"],
|
|
57
|
+
] as const) {
|
|
58
|
+
act(() => cell.focus());
|
|
59
|
+
fireEvent.keyDown(cell, { key });
|
|
60
|
+
fireEvent.keyDown(cell, { key: "c", ctrlKey: true });
|
|
61
|
+
expect(copyToClipboard).toHaveBeenLastCalledWith(value, undefined);
|
|
62
|
+
expect(cell).toHaveAttribute("aria-selected", "true");
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
it("moves focus and activation together inside a shadow root", () => {
|
|
68
|
+
vi.useFakeTimers();
|
|
69
|
+
const host = document.createElement("div");
|
|
70
|
+
document.body.append(host);
|
|
71
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
72
|
+
const container = document.createElement("div");
|
|
73
|
+
shadow.append(container);
|
|
74
|
+
const view = render(<SelectableTable />, { container });
|
|
75
|
+
try {
|
|
76
|
+
const [alice, bob] = within(container).getAllByRole("gridcell");
|
|
77
|
+
alice.scrollIntoView = vi.fn();
|
|
78
|
+
bob.scrollIntoView = vi.fn();
|
|
79
|
+
act(() => alice.focus());
|
|
80
|
+
fireEvent.keyDown(alice, { key: "ArrowDown" });
|
|
81
|
+
act(() => vi.runAllTimers());
|
|
82
|
+
expect(shadow.activeElement).toBe(bob);
|
|
83
|
+
fireEvent.keyDown(bob, { key: " " });
|
|
84
|
+
expect(bob).toHaveAttribute("aria-selected", "true");
|
|
85
|
+
expect(alice).toHaveAttribute("aria-selected", "false");
|
|
86
|
+
} finally {
|
|
87
|
+
view.unmount();
|
|
88
|
+
host.remove();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("does not move focus away from a nested control after navigation", () => {
|
|
93
|
+
vi.useFakeTimers();
|
|
94
|
+
render(<SelectableTable />);
|
|
95
|
+
const [alice, bob] = screen.getAllByRole("gridcell");
|
|
96
|
+
alice.scrollIntoView = vi.fn();
|
|
97
|
+
bob.scrollIntoView = vi.fn();
|
|
98
|
+
const button = document.createElement("button");
|
|
99
|
+
alice.append(button);
|
|
100
|
+
act(() => alice.focus());
|
|
101
|
+
fireEvent.keyDown(alice, { key: "ArrowDown" });
|
|
102
|
+
act(() => {
|
|
103
|
+
button.focus();
|
|
104
|
+
vi.runAllTimers();
|
|
105
|
+
});
|
|
106
|
+
expect(button).toHaveFocus();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("ignores stale scheduled focus when moving between cells", () => {
|
|
110
|
+
vi.useFakeTimers();
|
|
111
|
+
render(<SelectableTable />);
|
|
112
|
+
const [alice, bob] = screen.getAllByRole("gridcell");
|
|
113
|
+
alice.scrollIntoView = vi.fn();
|
|
114
|
+
bob.scrollIntoView = vi.fn();
|
|
115
|
+
act(() => {
|
|
116
|
+
alice.focus();
|
|
117
|
+
bob.focus();
|
|
118
|
+
vi.runAllTimers();
|
|
119
|
+
});
|
|
120
|
+
expect(bob).toHaveFocus();
|
|
121
|
+
expect(alice.scrollIntoView).not.toHaveBeenCalled();
|
|
122
|
+
expect(bob.scrollIntoView).toHaveBeenCalledOnce();
|
|
123
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
124
|
+
});
|
|
125
|
+
it.each([false, true])(
|
|
126
|
+
"selects focused cells without a preceding click (multiple=%s)",
|
|
127
|
+
(multiple) => {
|
|
128
|
+
render(<SelectableTable multiple={multiple} />);
|
|
129
|
+
const [alice, bob] = screen.getAllByRole("gridcell");
|
|
130
|
+
alice.focus();
|
|
131
|
+
fireEvent.keyDown(alice, { key: "Enter" });
|
|
132
|
+
expect(alice).toHaveAttribute("aria-selected", "true");
|
|
133
|
+
bob.focus();
|
|
134
|
+
fireEvent.keyDown(bob, { key: " " });
|
|
135
|
+
expect(bob).toHaveAttribute("aria-selected", "true");
|
|
136
|
+
expect(alice).toHaveAttribute("aria-selected", String(multiple));
|
|
137
|
+
fireEvent.keyDown(bob, { key: " " });
|
|
138
|
+
expect(bob).toHaveAttribute("aria-selected", "false");
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
it("does not intercept keys from embedded buttons", () => {
|
|
143
|
+
const select = vi.fn();
|
|
144
|
+
render(
|
|
145
|
+
<TooltipProvider>
|
|
146
|
+
<DataTable
|
|
147
|
+
data={[{ name: "Alice" }]}
|
|
148
|
+
columns={[
|
|
149
|
+
{
|
|
150
|
+
accessorKey: "name",
|
|
151
|
+
cell: () => <button type="button">Open person</button>,
|
|
152
|
+
},
|
|
153
|
+
]}
|
|
154
|
+
totalRows={1}
|
|
155
|
+
totalColumns={1}
|
|
156
|
+
pagination={false}
|
|
157
|
+
selection="single-cell"
|
|
158
|
+
onCellSelectionChange={select}
|
|
159
|
+
/>
|
|
160
|
+
</TooltipProvider>,
|
|
161
|
+
);
|
|
162
|
+
const button = screen.getByRole("button", { name: "Open person" });
|
|
163
|
+
for (const key of ["Enter", " ", "ArrowDown"]) {
|
|
164
|
+
expect(fireEvent.keyDown(button, { key })).toBe(true);
|
|
165
|
+
}
|
|
166
|
+
expect(select).not.toHaveBeenCalled();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("names search controls and clears a query", () => {
|
|
170
|
+
render(
|
|
171
|
+
<TooltipProvider>
|
|
172
|
+
<DataTable
|
|
173
|
+
data={[{ name: "Alice" }]}
|
|
174
|
+
columns={[{ accessorKey: "name" }]}
|
|
175
|
+
totalRows={1}
|
|
176
|
+
totalColumns={1}
|
|
177
|
+
pagination={false}
|
|
178
|
+
showSearch={true}
|
|
179
|
+
onSearchQueryChange={vi.fn()}
|
|
180
|
+
/>
|
|
181
|
+
</TooltipProvider>,
|
|
182
|
+
);
|
|
183
|
+
const search = screen.getByRole("textbox", { name: "Search table" });
|
|
184
|
+
fireEvent.change(search, { target: { value: "Alice" } });
|
|
185
|
+
fireEvent.click(screen.getByRole("button", { name: "Clear table search" }));
|
|
186
|
+
expect(search).toHaveValue("");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -402,6 +402,12 @@ const DataTableInternal = <TData,>({
|
|
|
402
402
|
</Banner>
|
|
403
403
|
)}
|
|
404
404
|
<Table
|
|
405
|
+
role={table.options.enableCellSelection ? "grid" : undefined}
|
|
406
|
+
aria-multiselectable={
|
|
407
|
+
table.options.enableCellSelection
|
|
408
|
+
? table.options.enableMultiCellSelection
|
|
409
|
+
: undefined
|
|
410
|
+
}
|
|
405
411
|
className={cn(
|
|
406
412
|
"relative",
|
|
407
413
|
columns.length <= AUTO_WIDTH_MAX_COLUMNS ? "w-auto" : "w-full",
|
|
@@ -244,15 +244,13 @@ const {
|
|
|
244
244
|
cellId: nextCell.id,
|
|
245
245
|
};
|
|
246
246
|
|
|
247
|
-
if (isShiftKey
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
state.selectedStartCell,
|
|
251
|
-
newCell,
|
|
252
|
-
);
|
|
247
|
+
if (isShiftKey) {
|
|
248
|
+
const anchor = state.selectedStartCell ?? currentCell;
|
|
249
|
+
const cellsInRange = getCellsBetween(table, anchor, newCell);
|
|
253
250
|
return {
|
|
254
251
|
...state,
|
|
255
252
|
selectedCells: new Set(cellsInRange),
|
|
253
|
+
selectedStartCell: anchor,
|
|
256
254
|
focusedCell: newCell,
|
|
257
255
|
};
|
|
258
256
|
} else {
|
|
@@ -45,45 +45,71 @@ export const useCellRangeSelection = <TData>({
|
|
|
45
45
|
},
|
|
46
46
|
);
|
|
47
47
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
break;
|
|
55
|
-
case "ArrowDown":
|
|
56
|
-
e.preventDefault();
|
|
57
|
-
navigate(e, "down");
|
|
58
|
-
break;
|
|
59
|
-
case "ArrowUp":
|
|
60
|
-
e.preventDefault();
|
|
61
|
-
navigate(e, "up");
|
|
62
|
-
break;
|
|
63
|
-
case "ArrowLeft":
|
|
64
|
-
e.preventDefault();
|
|
65
|
-
navigate(e, "left");
|
|
66
|
-
break;
|
|
67
|
-
case "ArrowRight":
|
|
68
|
-
e.preventDefault();
|
|
69
|
-
navigate(e, "right");
|
|
70
|
-
break;
|
|
71
|
-
case "Enter":
|
|
72
|
-
e.preventDefault();
|
|
73
|
-
actions.toggleCurrentRowSelection(table);
|
|
74
|
-
break;
|
|
75
|
-
case "Escape":
|
|
76
|
-
actions.clearSelection();
|
|
77
|
-
break;
|
|
78
|
-
case "a":
|
|
79
|
-
if (e.metaKey || e.ctrlKey) {
|
|
80
|
-
e.preventDefault();
|
|
81
|
-
actions.selectAllCells(table);
|
|
82
|
-
}
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
48
|
+
const handleCellFocus = useEvent((cell: Cell<TData, unknown>) => {
|
|
49
|
+
actions.setFocusedCell({
|
|
50
|
+
cellId: cell.id,
|
|
51
|
+
rowId: cell.row.id,
|
|
52
|
+
columnId: cell.column.id,
|
|
53
|
+
});
|
|
85
54
|
});
|
|
86
55
|
|
|
56
|
+
const handleCellsKeyDown = useEvent(
|
|
57
|
+
(e: React.KeyboardEvent<HTMLElement>, cell: Cell<TData, unknown>) => {
|
|
58
|
+
if (e.defaultPrevented || isInteractiveTarget(e)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
switch (e.key) {
|
|
62
|
+
case "c":
|
|
63
|
+
if (Events.isMetaOrCtrl(e)) {
|
|
64
|
+
handleCopy();
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
case "ArrowDown":
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
navigate(e, "down");
|
|
70
|
+
break;
|
|
71
|
+
case "ArrowUp":
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
navigate(e, "up");
|
|
74
|
+
break;
|
|
75
|
+
case "ArrowLeft":
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
navigate(e, "left");
|
|
78
|
+
break;
|
|
79
|
+
case "ArrowRight":
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
navigate(e, "right");
|
|
82
|
+
break;
|
|
83
|
+
case "Enter":
|
|
84
|
+
case " ":
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
updateSelection(
|
|
87
|
+
{
|
|
88
|
+
cellId: cell.id,
|
|
89
|
+
rowId: cell.row.id,
|
|
90
|
+
columnId: cell.column.id,
|
|
91
|
+
},
|
|
92
|
+
false,
|
|
93
|
+
);
|
|
94
|
+
if (table.options.enableCellSelection) {
|
|
95
|
+
cell.toggleSelected?.();
|
|
96
|
+
} else if (cell.row.getCanSelect()) {
|
|
97
|
+
cell.row.toggleSelected();
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
case "Escape":
|
|
101
|
+
actions.clearSelection();
|
|
102
|
+
break;
|
|
103
|
+
case "a":
|
|
104
|
+
if (e.metaKey || e.ctrlKey) {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
actions.selectAllCells(table);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
87
113
|
const handleCellMouseDown = useEvent(
|
|
88
114
|
(e: React.MouseEvent, cell: Cell<TData, unknown>) => {
|
|
89
115
|
// Right-clicks will trigger context menu, so avoid updating selected cells
|
|
@@ -122,6 +148,7 @@ export const useCellRangeSelection = <TData>({
|
|
|
122
148
|
handleCellMouseOver,
|
|
123
149
|
handleCopy,
|
|
124
150
|
handleCellsKeyDown,
|
|
151
|
+
handleCellFocus,
|
|
125
152
|
updateSelection,
|
|
126
153
|
clearSelection: actions.clearSelection,
|
|
127
154
|
};
|
|
@@ -148,7 +175,7 @@ const CONTENT_WRAPPER_MARIMO_TAGS: ReadonlySet<string> = new Set([
|
|
|
148
175
|
* shadow host (e.g. `<marimo-lazy>`), hiding any genuinely interactive
|
|
149
176
|
* descendants rendered inside the shadow tree.
|
|
150
177
|
*/
|
|
151
|
-
export function isInteractiveTarget(e: React.
|
|
178
|
+
export function isInteractiveTarget(e: React.SyntheticEvent): boolean {
|
|
152
179
|
const path: readonly EventTarget[] =
|
|
153
180
|
typeof e.nativeEvent?.composedPath === "function"
|
|
154
181
|
? e.nativeEvent.composedPath()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import { atom } from "jotai";
|
|
3
|
+
import { atom, useStore } from "jotai";
|
|
4
4
|
import { useJotaiEffect } from "@/core/state/jotai";
|
|
5
5
|
import { Logger } from "@/utils/Logger";
|
|
6
6
|
import { cellSelectionStateAtom } from "./atoms";
|
|
@@ -15,14 +15,21 @@ const focusedCellAtom = atom((get) => get(cellSelectionStateAtom).focusedCell);
|
|
|
15
15
|
export function useScrollIntoViewOnFocus(
|
|
16
16
|
root: React.RefObject<HTMLElement | null>,
|
|
17
17
|
) {
|
|
18
|
+
const store = useStore();
|
|
18
19
|
useJotaiEffect(focusedCellAtom, (focusedCell) => {
|
|
19
20
|
if (!focusedCell?.cellId) {
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const scroll = () => {
|
|
25
|
+
if (
|
|
26
|
+
!root.current ||
|
|
27
|
+
store.get(focusedCellAtom)?.cellId !== focusedCell.cellId
|
|
28
|
+
) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
24
31
|
// Check if this cell contains the CellRangeSelectionIndicator with our cellId
|
|
25
|
-
const indicator = root.current?.querySelector(
|
|
32
|
+
const indicator = root.current?.querySelector<HTMLElement>(
|
|
26
33
|
`[data-cell-id="${focusedCell.cellId}"]`,
|
|
27
34
|
);
|
|
28
35
|
if (!indicator) {
|
|
@@ -33,6 +40,18 @@ export function useScrollIntoViewOnFocus(
|
|
|
33
40
|
return;
|
|
34
41
|
}
|
|
35
42
|
|
|
43
|
+
const focusRoot = root.current.getRootNode();
|
|
44
|
+
const activeElement =
|
|
45
|
+
focusRoot instanceof ShadowRoot || focusRoot instanceof Document
|
|
46
|
+
? focusRoot.activeElement
|
|
47
|
+
: null;
|
|
48
|
+
if (
|
|
49
|
+
root.current.contains(activeElement) &&
|
|
50
|
+
activeElement?.matches("td[data-cell-id]")
|
|
51
|
+
) {
|
|
52
|
+
indicator.focus({ preventScroll: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
36
55
|
indicator.scrollIntoView({
|
|
37
56
|
behavior: "smooth",
|
|
38
57
|
block: "nearest",
|
|
@@ -124,6 +124,7 @@ export const DataTableBody = <TData,>({
|
|
|
124
124
|
handleCellMouseUp,
|
|
125
125
|
handleCellMouseOver,
|
|
126
126
|
handleCellsKeyDown,
|
|
127
|
+
handleCellFocus,
|
|
127
128
|
handleCopy: handleCopyAllCells,
|
|
128
129
|
} = useCellRangeSelection({ table });
|
|
129
130
|
|
|
@@ -147,10 +148,16 @@ export const DataTableBody = <TData,>({
|
|
|
147
148
|
return (
|
|
148
149
|
<TableCell
|
|
149
150
|
tabIndex={0}
|
|
151
|
+
role={table.options.enableCellSelection ? "gridcell" : undefined}
|
|
152
|
+
aria-selected={
|
|
153
|
+
table.options.enableCellSelection
|
|
154
|
+
? cell.getIsSelected?.()
|
|
155
|
+
: undefined
|
|
156
|
+
}
|
|
150
157
|
{...getCellDomProps(cell.id)}
|
|
151
158
|
key={cell.id}
|
|
152
159
|
className={cn(
|
|
153
|
-
"whitespace-pre truncate outline-hidden border-r border-r-border/75",
|
|
160
|
+
"whitespace-pre truncate outline-hidden focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring border-r border-r-border/75",
|
|
154
161
|
!fixedWidth && "max-w-[300px]",
|
|
155
162
|
cell.column.getColumnWrapping &&
|
|
156
163
|
cell.column.getColumnWrapping?.() === "wrap" &&
|
|
@@ -169,7 +176,13 @@ export const DataTableBody = <TData,>({
|
|
|
169
176
|
hoverTooltip.handleCellMouseOver(e, cell);
|
|
170
177
|
}}
|
|
171
178
|
onMouseLeave={hoverTooltip.handleCellMouseLeave}
|
|
172
|
-
onFocus={(e) =>
|
|
179
|
+
onFocus={(e) => {
|
|
180
|
+
if (e.target === e.currentTarget) {
|
|
181
|
+
handleCellFocus(cell);
|
|
182
|
+
}
|
|
183
|
+
hoverTooltip.handleCellFocus(e, cell);
|
|
184
|
+
}}
|
|
185
|
+
onKeyDown={(e) => handleCellsKeyDown(e, cell)}
|
|
173
186
|
onBlur={hoverTooltip.handleCellBlur}
|
|
174
187
|
onContextMenu={() => handleContextMenu(cell)}
|
|
175
188
|
>
|
|
@@ -264,11 +277,7 @@ export const DataTableBody = <TData,>({
|
|
|
264
277
|
return rows.map((row) => renderRow(row));
|
|
265
278
|
};
|
|
266
279
|
|
|
267
|
-
const tableBody = (
|
|
268
|
-
<TableBody onKeyDown={handleCellsKeyDown} ref={tableRef}>
|
|
269
|
-
{renderRows()}
|
|
270
|
-
</TableBody>
|
|
271
|
-
);
|
|
280
|
+
const tableBody = <TableBody ref={tableRef}>{renderRows()}</TableBody>;
|
|
272
281
|
|
|
273
282
|
return (
|
|
274
283
|
<>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
4
|
import { Provider } from "jotai";
|
|
5
5
|
import { describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
@@ -17,6 +17,29 @@ const renderWithProviders = (component: React.ReactNode) => {
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
describe("RowExpandedPanel", () => {
|
|
20
|
+
it("preserves Space activation on navigation buttons", async () => {
|
|
21
|
+
const select = vi.fn();
|
|
22
|
+
const navigate = vi.fn();
|
|
23
|
+
renderWithProviders(
|
|
24
|
+
<RowViewerPanel
|
|
25
|
+
rowIdx={0}
|
|
26
|
+
setRowIdx={navigate}
|
|
27
|
+
totalRows={2}
|
|
28
|
+
fieldTypes={[["name", ["string", "string"]]]}
|
|
29
|
+
getRow={async () => ({ rows: [{ name: "Alice" }] })}
|
|
30
|
+
isSelectable={true}
|
|
31
|
+
isRowSelected={false}
|
|
32
|
+
handleRowSelectionChange={select}
|
|
33
|
+
/>,
|
|
34
|
+
);
|
|
35
|
+
await screen.findByText("Alice");
|
|
36
|
+
const next = screen.getByRole("button", { name: "Next row" });
|
|
37
|
+
expect(fireEvent.keyDown(next, { key: " " })).toBe(true);
|
|
38
|
+
expect(select).not.toHaveBeenCalled();
|
|
39
|
+
fireEvent.click(next);
|
|
40
|
+
expect(navigate).toHaveBeenCalledExactlyOnceWith(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
20
43
|
const mockFieldTypes: FieldTypesWithExternalType = [
|
|
21
44
|
["name", ["string", "string"]],
|
|
22
45
|
["age", ["number", "number"]],
|
|
@@ -37,6 +37,7 @@ import type { GetRowResult } from "@/plugins/impl/DataTablePlugin";
|
|
|
37
37
|
import { Objects } from "@/utils/objects";
|
|
38
38
|
import { NAMELESS_COLUMN_PREFIX, renderCellValue } from "../columns";
|
|
39
39
|
import { prettifyRowCount } from "../pagination";
|
|
40
|
+
import { isInteractiveTarget } from "../range-focus/use-cell-range-selection";
|
|
40
41
|
import {
|
|
41
42
|
type FieldTypesWithExternalType,
|
|
42
43
|
INDEX_COLUMN_NAME,
|
|
@@ -102,8 +103,7 @@ export const RowViewerPanel: React.FC<RowViewerPanelProps> = ({
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
105
|
-
|
|
106
|
-
if (e.target instanceof HTMLInputElement) {
|
|
106
|
+
if (e.defaultPrevented || isInteractiveTarget(e)) {
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
109
109
|
switch (e.key) {
|
|
@@ -284,6 +284,7 @@ export const DataSources: React.FC = () => {
|
|
|
284
284
|
{hasSearch && (
|
|
285
285
|
<button
|
|
286
286
|
type="button"
|
|
287
|
+
aria-label="Clear table search"
|
|
287
288
|
className="float-right border-b px-2 m-0 h-full hover:bg-accent hover:text-accent-foreground"
|
|
288
289
|
onClick={() => setSearchValue("")}
|
|
289
290
|
>
|