Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions src/components/ProcessTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,39 +96,49 @@ export function ProcessTable({ processes, filter }: ProcessTableProps) {
};

return (
<div className="process-table">
<div className="table-header">
<div className="process-table" role="table" aria-label="Process list">
<div className="table-header" role="row">
<span
role="columnheader"
className="col-pid sortable"
onClick={() => handleSort('pid')}
aria-sort={sortField === 'pid' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
>
PID{getSortIndicator('pid')}
</span>
<span
role="columnheader"
className="col-user sortable"
onClick={() => handleSort('user')}
aria-sort={sortField === 'user' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
>
USER{getSortIndicator('user')}
</span>
<span
role="columnheader"
className="col-cpu sortable"
onClick={() => handleSort('cpu')}
aria-sort={sortField === 'cpu' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
>
CPU%{getSortIndicator('cpu')}
</span>
<span
role="columnheader"
className="col-mem sortable"
onClick={() => handleSort('mem')}
aria-sort={sortField === 'mem' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
>
MEM%{getSortIndicator('mem')}
</span>
<span className="col-virt">VIRT</span>
<span className="col-res">RES</span>
<span className="col-state">S</span>
<span className="col-time">TIME</span>
<span role="columnheader" className="col-virt">VIRT</span>
<span role="columnheader" className="col-res">RES</span>
<span role="columnheader" className="col-state">S</span>
<span role="columnheader" className="col-time">TIME</span>
<span
role="columnheader"
className="col-command sortable"
onClick={() => handleSort('command')}
aria-sort={sortField === 'command' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
>
Comment on lines 101 to 142
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/components/ProcessTable.tsx | head -200 | tail -120

Repository: coderabbit-demo/btop

Length of output: 5370


🌐 Web query:

ARIA sortable column header keyboard accessibility tabindex enter space

💡 Result:

  • Put the sorting control in the header as a real <button> (e.g., wrap the header text in a button inside <th>). Then you get keyboard focus via Tab and activation with Enter/Space for free—no tabindex needed on the <th>. (w3.org)
  • Use aria-sort on the <th> of the currently sorted column only, toggling ascending/descending as sort changes, and move/remove aria-sort when a different column becomes sorted. (w3.org)
  • Don’t rely on role="columnheader" for keyboard behavior: columnheader has no special keyboard interaction; the button is what must be operable. (developer.mozilla.org)

Minimal pattern:

<th scope="col" aria-sort="ascending">
  <button type="button">Company</button>
</th>
<th scope="col">
  <button type="button">Last name</button>
</th>

If you can’t use a real <button>, make the control focusable and clickable:

  • tabindex="0", role="button", and handle Enter + Space (typically keydown for Space with preventDefault() so the page doesn’t scroll).

Citations:


Sortable headers lack keyboard accessibility.

The sortable column headers (pid, user, cpu, mem, command at lines 101–142) have onClick handlers but are not focusable and cannot be activated via keyboard. Add tabIndex={0} and onKeyDown handlers for Enter/Space to all five sortable headers, or replace with <button> elements inside the columnheader spans for built-in keyboard support.

Proposed fix (apply to all sortable headers)
         <span
           role="columnheader"
           className="col-pid sortable"
           onClick={() => handleSort('pid')}
+          tabIndex={0}
+          onKeyDown={(e) => {
+            if (e.key === 'Enter' || e.key === ' ') {
+              e.preventDefault();
+              handleSort('pid');
+            }
+          }}
           aria-sort={sortField === 'pid' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
         >
           PID{getSortIndicator('pid')}
         </span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span
role="columnheader"
className="col-pid sortable"
onClick={() => handleSort('pid')}
aria-sort={sortField === 'pid' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
>
PID{getSortIndicator('pid')}
</span>
<span
role="columnheader"
className="col-user sortable"
onClick={() => handleSort('user')}
aria-sort={sortField === 'user' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
>
USER{getSortIndicator('user')}
</span>
<span
role="columnheader"
className="col-cpu sortable"
onClick={() => handleSort('cpu')}
aria-sort={sortField === 'cpu' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
>
CPU%{getSortIndicator('cpu')}
</span>
<span
role="columnheader"
className="col-mem sortable"
onClick={() => handleSort('mem')}
aria-sort={sortField === 'mem' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
>
MEM%{getSortIndicator('mem')}
</span>
<span className="col-virt">VIRT</span>
<span className="col-res">RES</span>
<span className="col-state">S</span>
<span className="col-time">TIME</span>
<span role="columnheader" className="col-virt">VIRT</span>
<span role="columnheader" className="col-res">RES</span>
<span role="columnheader" className="col-state">S</span>
<span role="columnheader" className="col-time">TIME</span>
<span
role="columnheader"
className="col-command sortable"
onClick={() => handleSort('command')}
aria-sort={sortField === 'command' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
>
<span
role="columnheader"
className="col-pid sortable"
onClick={() => handleSort('pid')}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleSort('pid');
}
}}
aria-sort={sortField === 'pid' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
>
PID{getSortIndicator('pid')}
</span>
<span
role="columnheader"
className="col-user sortable"
onClick={() => handleSort('user')}
aria-sort={sortField === 'user' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
>
USER{getSortIndicator('user')}
</span>
<span
role="columnheader"
className="col-cpu sortable"
onClick={() => handleSort('cpu')}
aria-sort={sortField === 'cpu' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
>
CPU%{getSortIndicator('cpu')}
</span>
<span
role="columnheader"
className="col-mem sortable"
onClick={() => handleSort('mem')}
aria-sort={sortField === 'mem' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
>
MEM%{getSortIndicator('mem')}
</span>
<span role="columnheader" className="col-virt">VIRT</span>
<span role="columnheader" className="col-res">RES</span>
<span role="columnheader" className="col-state">S</span>
<span role="columnheader" className="col-time">TIME</span>
<span
role="columnheader"
className="col-command sortable"
onClick={() => handleSort('command')}
aria-sort={sortField === 'command' ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ProcessTable.tsx` around lines 101 - 142, The sortable column
header spans (those with class "sortable" for PID, USER, CPU%, MEM%, and
COMMAND) are not keyboard-accessible; update each span (e.g., the spans calling
handleSort('pid'), handleSort('user'), handleSort('cpu'), handleSort('mem'),
handleSort('command')) to be focusable by adding tabIndex={0} and add an
onKeyDown handler that listens for Enter and Space and invokes the same
handleSort(...) call; ensure existing aria-sort logic using sortField and
sortDirection remains unchanged so screen readers still get state.

COMMAND{getSortIndicator('command')}
</span>
Expand All @@ -137,8 +147,10 @@ export function ProcessTable({ processes, filter }: ProcessTableProps) {
{filteredAndSortedProcesses.map((process) => (
<div
key={process.pid}
role="row"
className={`table-row ${selectedPid === process.pid ? 'selected' : ''}`}
onClick={() => setSelectedPid(process.pid === selectedPid ? null : process.pid)}
aria-selected={selectedPid === process.pid}
>
<span className="col-pid">{process.pid}</span>
<span className="col-user">{process.user.substring(0, 8)}</span>
Expand Down
Loading