diff --git a/changelog.d/fixes/11610-datatable-keyboard-activation.md b/changelog.d/fixes/11610-datatable-keyboard-activation.md
new file mode 100644
index 0000000000..a51f2340d7
--- /dev/null
+++ b/changelog.d/fixes/11610-datatable-keyboard-activation.md
@@ -0,0 +1 @@
+- **fix(dashboard):** Enable Enter and Space activation for clickable data-table rows without hijacking nested controls ([#11610](https://github.com/diegosouzapw/OmniRoute/pull/11610)) — thanks @pacocartones
diff --git a/src/shared/components/DataTable.tsx b/src/shared/components/DataTable.tsx
index b10961ac61..7f1ce71c96 100644
--- a/src/shared/components/DataTable.tsx
+++ b/src/shared/components/DataTable.tsx
@@ -2,6 +2,21 @@
import { useTranslations } from "next-intl";
+const INTERACTIVE_ELEMENT_SELECTOR =
+ 'a[href], button, input, select, textarea, summary, [role="button"], [role="link"], ' +
+ '[contenteditable]:not([contenteditable="false"]), [tabindex]:not([tabindex="-1"])';
+
+function targetsNestedInteractiveElement(
+ row: HTMLTableRowElement,
+ target: EventTarget | null
+): boolean {
+ if (!(target instanceof Element)) return false;
+ const interactiveElement = target.closest(INTERACTIVE_ELEMENT_SELECTOR);
+ return (
+ interactiveElement !== null && interactiveElement !== row && row.contains(interactiveElement)
+ );
+}
+
/**
* DataTable — Shared UI primitive (T-29)
*
@@ -148,7 +163,25 @@ export default function DataTable({
{data.map((row, idx) => (
onRowClick?.(row)}
+ onClick={(event) => {
+ if (
+ !onRowClick ||
+ targetsNestedInteractiveElement(event.currentTarget, event.target)
+ )
+ return;
+ onRowClick(row);
+ }}
+ onKeyDown={
+ onRowClick
+ ? (event) => {
+ if (event.target !== event.currentTarget) return;
+ if (event.key !== "Enter" && event.key !== " ") return;
+ event.preventDefault();
+ onRowClick(row);
+ }
+ : undefined
+ }
+ tabIndex={onRowClick ? 0 : undefined}
style={{
cursor: onRowClick ? "pointer" : "default",
background:
diff --git a/tests/unit/ui/data-table-keyboard-activation.test.tsx b/tests/unit/ui/data-table-keyboard-activation.test.tsx
new file mode 100644
index 0000000000..ff0b5e1d02
--- /dev/null
+++ b/tests/unit/ui/data-table-keyboard-activation.test.tsx
@@ -0,0 +1,125 @@
+// @vitest-environment jsdom
+import React, { act } from "react";
+import { createRoot } from "react-dom/client";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import type { DataTableRow } from "../../../src/shared/components/DataTable";
+
+vi.mock("next-intl", () => ({
+ useTranslations: () => (key: string) => key,
+}));
+
+const { default: DataTable } = await import("../../../src/shared/components/DataTable");
+
+const cleanups: Array<() => void> = [];
+const columns = [{ key: "name", label: "Name" }];
+const data: DataTableRow[] = [{ id: "row-1", name: "Alpha" }];
+
+function renderTable(onRowClick?: (row: DataTableRow) => void, withButton = false, rows = data) {
+ const container = document.createElement("div");
+ document.body.appendChild(container);
+ const root = createRoot(container);
+ act(() => {
+ root.render(
+
+ withButton ? : String(row.name)
+ }
+ />
+ );
+ });
+ cleanups.push(() => {
+ act(() => root.unmount());
+ container.remove();
+ });
+ return container;
+}
+
+beforeEach(() => {
+ (
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+ ).IS_REACT_ACT_ENVIRONMENT = true;
+});
+
+afterEach(() => {
+ while (cleanups.length) cleanups.pop()!();
+});
+
+describe("DataTable keyboard row activation", () => {
+ it.each(["Enter", " "])("makes clickable rows focusable and activates them with %j", (key) => {
+ const onRowClick = vi.fn();
+ const container = renderTable(onRowClick);
+ const row = container.querySelector("tbody tr")!;
+ const event = new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true });
+
+ expect(row.tabIndex).toBe(0);
+ act(() => {
+ row.dispatchEvent(event);
+ });
+
+ expect(event.defaultPrevented).toBe(true);
+ expect(onRowClick).toHaveBeenCalledOnce();
+ expect(onRowClick).toHaveBeenCalledWith(data[0]);
+ });
+
+ it("does not make passive rows keyboard-interactive", () => {
+ const container = renderTable();
+ const row = container.querySelector("tbody tr")!;
+
+ expect(row.getAttribute("tabindex")).toBeNull();
+ });
+
+ it("does not hijack keyboard events from controls rendered inside a row", () => {
+ const onRowClick = vi.fn();
+ const container = renderTable(onRowClick, true);
+ const button = container.querySelector("tbody button")!;
+
+ act(() => {
+ button.dispatchEvent(
+ new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true })
+ );
+ button.click();
+ });
+
+ expect(onRowClick).not.toHaveBeenCalled();
+ });
+
+ it("preserves mouse activation and ignores unrelated keys", () => {
+ const onRowClick = vi.fn();
+ const container = renderTable(onRowClick);
+ const row = container.querySelector("tbody tr")!;
+
+ act(() => {
+ row.dispatchEvent(
+ new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true })
+ );
+ });
+ expect(onRowClick).not.toHaveBeenCalled();
+
+ const cell = row.querySelector("td")!;
+ act(() => cell.click());
+ expect(onRowClick).toHaveBeenCalledOnce();
+ expect(onRowClick).toHaveBeenCalledWith(data[0]);
+ });
+
+ it("activates the focused row instead of another row", () => {
+ const onRowClick = vi.fn();
+ const rows: DataTableRow[] = [
+ { id: "row-1", name: "Alpha" },
+ { id: "row-2", name: "Beta" },
+ ];
+ const container = renderTable(onRowClick, false, rows);
+ const renderedRows = container.querySelectorAll("tbody tr");
+
+ act(() => {
+ renderedRows[1]!.dispatchEvent(
+ new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true })
+ );
+ });
+
+ expect(onRowClick).toHaveBeenCalledOnce();
+ expect(onRowClick).toHaveBeenCalledWith(rows[1]);
+ });
+});