mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-23 18:57:09 +08:00
fix(a11y): fix traces page accessibility violations — rowgroup tabbable, button name, and heading order
This commit is contained in:
@ -32,10 +32,10 @@ const Alert = React.forwardRef<
|
||||
Alert.displayName = "Alert";
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
|
||||
@ -1938,6 +1938,8 @@
|
||||
"trace.clearAll": "Alles löschen",
|
||||
"trace.clearAllConfirm": "Möchten Sie wirklich alle Datensätze löschen? Dadurch werden alle zugehörigen Flow-Aktivitätsverläufe endgültig gelöscht; dieser Vorgang kann nicht rückgängig gemacht werden.",
|
||||
"trace.clearAllRecords": "Alle Datensätze löschen",
|
||||
"trace.tableAriaLabel": "Trace runs table",
|
||||
"trace.statusFilterLabel": "Filter by status",
|
||||
"trace.clearDates": "Termine löschen",
|
||||
"trace.clearError": "Fehler beim Löschen von Datensätzen",
|
||||
"trace.clearedSuccess": "Datensätze wurden erfolgreich gelöscht",
|
||||
|
||||
@ -1325,6 +1325,8 @@
|
||||
"trace.dateRangeLabel": "{{start}} - {{end}}",
|
||||
"trace.invalidDateRangeMessage": "End date cannot be earlier than start date.",
|
||||
"trace.clearDates": "Clear dates",
|
||||
"trace.tableAriaLabel": "Trace runs table",
|
||||
"trace.statusFilterLabel": "Filter by status",
|
||||
"flow.restoreVersion": "Restore this version of your flow",
|
||||
"flow.saveVersion": "Save a version of your flow",
|
||||
"flow.moreOptions": "More options",
|
||||
|
||||
@ -1938,6 +1938,8 @@
|
||||
"trace.clearAll": "Borrar todo",
|
||||
"trace.clearAllConfirm": "¿Estás seguro de que quieres borrar todos los registros? Esto eliminará de forma permanente todos los registros de actividad de Flow relacionados y no se podrá deshacer.",
|
||||
"trace.clearAllRecords": "Borrar todos los registros",
|
||||
"trace.tableAriaLabel": "Trace runs table",
|
||||
"trace.statusFilterLabel": "Filter by status",
|
||||
"trace.clearDates": "Borrar fechas",
|
||||
"trace.clearError": "Error al borrar registros",
|
||||
"trace.clearedSuccess": "Los registros se han borrado correctamente",
|
||||
|
||||
@ -1938,6 +1938,8 @@
|
||||
"trace.clearAll": "Effacer tout",
|
||||
"trace.clearAllConfirm": "Êtes-vous sûr de vouloir effacer tous les enregistrements? Cette opération supprimera définitivement toutes les traces d'activité de flux associées et ne pourra pas être annulée.",
|
||||
"trace.clearAllRecords": "Effacer tous les enregistrements",
|
||||
"trace.tableAriaLabel": "Trace runs table",
|
||||
"trace.statusFilterLabel": "Filter by status",
|
||||
"trace.clearDates": "Effacer les dates",
|
||||
"trace.clearError": "Erreur lors de la suppression des enregistrements",
|
||||
"trace.clearedSuccess": "Les enregistrements ont été effacés avec succès",
|
||||
|
||||
@ -1938,6 +1938,8 @@
|
||||
"trace.clearAll": "すべて消去",
|
||||
"trace.clearAllConfirm": "すべての記録を削除してもよろしいですか? これにより、関連するすべてのフローアクティビティトレースが完全に削除され、元に戻すことはできません。",
|
||||
"trace.clearAllRecords": "すべての記録を消去",
|
||||
"trace.tableAriaLabel": "Trace runs table",
|
||||
"trace.statusFilterLabel": "Filter by status",
|
||||
"trace.clearDates": "日付を消去",
|
||||
"trace.clearError": "レコードの削除中にエラーが発生しました",
|
||||
"trace.clearedSuccess": "レコードのクリアに成功しました",
|
||||
|
||||
@ -1938,6 +1938,8 @@
|
||||
"trace.clearAll": "Limpar Tudo",
|
||||
"trace.clearAllConfirm": "Tem certeza de que deseja apagar todos os registros? Isso excluirá permanentemente todos os rastros de atividade do fluxo relacionados e não poderá ser desfeito.",
|
||||
"trace.clearAllRecords": "Limpar todos os registros",
|
||||
"trace.tableAriaLabel": "Trace runs table",
|
||||
"trace.statusFilterLabel": "Filter by status",
|
||||
"trace.clearDates": "Limpar datas",
|
||||
"trace.clearError": "Erro ao limpar registros",
|
||||
"trace.clearedSuccess": "Os registros foram apagados com sucesso",
|
||||
|
||||
@ -1938,6 +1938,8 @@
|
||||
"trace.clearAll": "全部清除",
|
||||
"trace.clearAllConfirm": "您确定要清除所有记录吗? 这将永久删除所有相关的流程活动跟踪记录,且无法撤销。",
|
||||
"trace.clearAllRecords": "清除所有记录",
|
||||
"trace.tableAriaLabel": "Trace runs table",
|
||||
"trace.statusFilterLabel": "Filter by status",
|
||||
"trace.clearDates": "清除日期",
|
||||
"trace.clearError": "清除记录时发生错误",
|
||||
"trace.clearedSuccess": "记录已成功清除",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { CellClickedEvent } from "ag-grid-community";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import type { CellClickedEvent, CellKeyDownEvent } from "ag-grid-community";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import IconComponent from "@/components/common/genericIconComponent";
|
||||
@ -182,6 +182,30 @@ export function FlowInsightsContent({
|
||||
setTracePanelOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleCellKeyDown = useCallback(
|
||||
(event: CellKeyDownEvent) => {
|
||||
const keyboardEvent = event.event as KeyboardEvent | undefined;
|
||||
if (keyboardEvent?.key === "Enter") {
|
||||
const rowData = event.data as TraceListItem | undefined;
|
||||
setTracePanelTraceId(rowData?.id ?? null);
|
||||
setTracePanelOpen(true);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const traceTableRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const applyRowTabIndices = useCallback((containerEl: HTMLElement | null) => {
|
||||
if (!containerEl) return;
|
||||
const bodyRows = containerEl.querySelectorAll<HTMLElement>(
|
||||
".ag-center-cols-container [role='row']",
|
||||
);
|
||||
bodyRows.forEach((row, idx) => {
|
||||
row.setAttribute("tabindex", idx === 0 ? "0" : "-1");
|
||||
});
|
||||
}, []);
|
||||
|
||||
const totalRuns = tracesData?.total ?? rows.length;
|
||||
const totalPages = Math.max(
|
||||
1,
|
||||
@ -306,7 +330,10 @@ export function FlowInsightsContent({
|
||||
</div>
|
||||
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="h-8 w-[130px] [&>span]:truncate">
|
||||
<SelectTrigger
|
||||
className="h-8 w-[130px] [&>span]:truncate"
|
||||
aria-label={t("trace.statusFilterLabel")}
|
||||
>
|
||||
<SelectValue placeholder={t("trace.allStatus")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@ -391,7 +418,10 @@ export function FlowInsightsContent({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ag-flush-mode flex-1 overflow-hidden">
|
||||
<div
|
||||
ref={traceTableRef}
|
||||
className="ag-flush-mode flex-1 overflow-hidden"
|
||||
>
|
||||
{groupBySession ? (
|
||||
renderGroupedSessionContent({
|
||||
groupedRows,
|
||||
@ -404,6 +434,7 @@ export function FlowInsightsContent({
|
||||
<TableComponent
|
||||
key="Executions"
|
||||
readOnlyEdit
|
||||
aria-label={t("trace.tableAriaLabel")}
|
||||
className="h-max-full h-full w-full"
|
||||
data-testid="flow-insights-trace-table"
|
||||
pagination={false}
|
||||
@ -412,6 +443,13 @@ export function FlowInsightsContent({
|
||||
rowData={rows}
|
||||
headerHeight={rows.length === 0 ? 0 : undefined}
|
||||
onCellClicked={handleCellClicked}
|
||||
onCellKeyDown={handleCellKeyDown}
|
||||
onFirstDataRendered={() =>
|
||||
applyRowTabIndices(traceTableRef.current)
|
||||
}
|
||||
onRowDataUpdated={() =>
|
||||
applyRowTabIndices(traceTableRef.current)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
// Unit tests for the applyRowTabIndices logic used in FlowInsightsContent.
|
||||
// The fix: after AG Grid renders rows, we set tabindex="0" on the first row
|
||||
// and tabindex="-1" on all others so that the IBM accessibility checker finds
|
||||
// a tabbable descendant inside role="rowgroup".
|
||||
|
||||
function applyRowTabIndices(containerEl: HTMLElement | null): void {
|
||||
if (!containerEl) return;
|
||||
const bodyRows = containerEl.querySelectorAll<HTMLElement>(
|
||||
".ag-center-cols-container [role='row']",
|
||||
);
|
||||
bodyRows.forEach((row, idx) => {
|
||||
row.setAttribute("tabindex", idx === 0 ? "0" : "-1");
|
||||
});
|
||||
}
|
||||
|
||||
function makeGrid(rowCount: number): HTMLElement {
|
||||
const container = document.createElement("div");
|
||||
const colsContainer = document.createElement("div");
|
||||
colsContainer.className = "ag-center-cols-container";
|
||||
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const row = document.createElement("div");
|
||||
row.setAttribute("role", "row");
|
||||
colsContainer.appendChild(row);
|
||||
}
|
||||
|
||||
container.appendChild(colsContainer);
|
||||
return container;
|
||||
}
|
||||
|
||||
describe("applyRowTabIndices — rowgroup tabbable fix", () => {
|
||||
it("sets tabindex=0 on the first row and -1 on all others", () => {
|
||||
const container = makeGrid(3);
|
||||
applyRowTabIndices(container);
|
||||
|
||||
const rows = container.querySelectorAll<HTMLElement>("[role='row']");
|
||||
expect(rows[0].getAttribute("tabindex")).toBe("0");
|
||||
expect(rows[1].getAttribute("tabindex")).toBe("-1");
|
||||
expect(rows[2].getAttribute("tabindex")).toBe("-1");
|
||||
});
|
||||
|
||||
it("handles a single row — that row gets tabindex=0", () => {
|
||||
const container = makeGrid(1);
|
||||
applyRowTabIndices(container);
|
||||
|
||||
const rows = container.querySelectorAll<HTMLElement>("[role='row']");
|
||||
expect(rows[0].getAttribute("tabindex")).toBe("0");
|
||||
});
|
||||
|
||||
it("does nothing when containerEl is null", () => {
|
||||
expect(() => applyRowTabIndices(null)).not.toThrow();
|
||||
});
|
||||
|
||||
it("does nothing when there are no rows", () => {
|
||||
const container = makeGrid(0);
|
||||
applyRowTabIndices(container);
|
||||
|
||||
const rows = container.querySelectorAll("[role='row']");
|
||||
expect(rows.length).toBe(0);
|
||||
});
|
||||
|
||||
it("overwrites any existing tabindex on rows", () => {
|
||||
const container = makeGrid(2);
|
||||
const rows = container.querySelectorAll<HTMLElement>("[role='row']");
|
||||
rows[0].setAttribute("tabindex", "5");
|
||||
rows[1].setAttribute("tabindex", "5");
|
||||
|
||||
applyRowTabIndices(container);
|
||||
|
||||
expect(rows[0].getAttribute("tabindex")).toBe("0");
|
||||
expect(rows[1].getAttribute("tabindex")).toBe("-1");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user