Skip to content

upstreamable: frontend: Table: Implement keyboard accessible column selector in ColumnVisibilityButton#316

Open
sniok wants to merge 1 commit intoAzure:headlamp-downstreamfrom
sniok:table-a11y-column-selector
Open

upstreamable: frontend: Table: Implement keyboard accessible column selector in ColumnVisibilityButton#316
sniok wants to merge 1 commit intoAzure:headlamp-downstreamfrom
sniok:table-a11y-column-selector

Conversation

@sniok
Copy link
Collaborator

@sniok sniok commented Feb 24, 2026

Fixes #309

The built-in MRT_ShowHideColumnsButton renders column visibility toggles using FormControlLabel with Switch wrapped inside Tooltip, which breaks keyboard navigation. The menu items don't respond to Enter/Space and the Hide All/Show All buttons are unreachable via arrow keys.

image

Changes

  • Add a custom ColumnVisibilityButton component that replaces MRT's built-in column visibility menu with fully keyboard-accessible MenuItem elements
  • Override renderToolbarInternalActions in Table.tsx to render our custom button alongside MRT's filter toggle buttons
  • All menu items (Hide All, Show All, individual column toggles) are navigable via arrow keys and activatable via Enter/Space

How to test:

  1. Find any resourcetable (clusters/resources)
  2. Navigate with keyboard to "Show/Hide column"
  3. Ensure every item is accessible with keyboard (space/enter to open popup, arrow keys to navigate, space/enter to select column)

Copilot AI review requested due to automatic review settings February 24, 2026 22:22
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses keyboard accessibility gaps in the table column visibility UI by replacing Material React Table’s built-in “Show/Hide Columns” menu with a custom, keyboard-navigable implementation and wiring it into the shared Table wrapper.

Changes:

  • Added a custom ColumnVisibilityButton that renders column visibility controls as MenuItems for proper arrow-key navigation and Enter/Space activation.
  • Updated Table.tsx to render MRT’s filter toggle buttons and the new column visibility button via renderToolbarInternalActions.
  • Preserved row-selection toolbar rendering when rows are selected.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
frontend/src/components/common/Table/Table.tsx Hooks custom toolbar internal actions into the shared table wrapper and renders accessible filter/column-visibility controls.
frontend/src/components/common/Table/ColumnVisibilityButton.tsx New accessible column visibility menu implementation using MUI Menu/MenuItem interactions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@illume illume added the a11y a11y related issues label Feb 25, 2026
@sniok sniok force-pushed the table-a11y-column-selector branch from f52c08b to fcc43ef Compare February 25, 2026 15:52
Copy link
Collaborator

@skoeva skoeva left a comment

Choose a reason for hiding this comment

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

looks like some of the snapshots are outdated

Copilot AI review requested due to automatic review settings February 26, 2026 10:56
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 138 out of 138 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +295 to +300
initialState: initState,
} = tbl.options;

return (
<>
{enableFilters && enableGlobalFilter && !initState?.showGlobalFilter && (
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

MRT_ToggleGlobalFilterButton is only rendered when !initState?.showGlobalFilter. If a table sets initialState.showGlobalFilter = true (e.g., when a default global filter is provided), the toggle button disappears entirely and users cannot hide/show the global filter via the toolbar. Consider rendering the toggle whenever enableGlobalFilter is true (or basing the condition on current table state rather than initialState).

Suggested change
initialState: initState,
} = tbl.options;
return (
<>
{enableFilters && enableGlobalFilter && !initState?.showGlobalFilter && (
} = tbl.options;
return (
<>
{enableFilters && enableGlobalFilter && (

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +60
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const menuId = 'column-visibility-menu';
const isOpen = !!anchorEl;
const {
icons: { ViewColumnIcon },
localization,
} = table.options;

const columns = table
.getAllLeafColumns()
.filter(
col =>
col.columnDef.enableHiding !== false &&
col.columnDef.visibleInShowHideMenu !== false &&
col.columnDef.columnDefType !== 'display'
);

const handleToggleAll = (visible: boolean) => {
columns.filter(col => col.getCanHide()).forEach(col => col.toggleVisibility(visible));
};

return (
<>
<Tooltip title={localization.showHideColumns}>
<IconButton
aria-label={localization.showHideColumns}
aria-haspopup="menu"
aria-expanded={isOpen}
aria-controls={isOpen ? menuId : undefined}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

menuId is hard-coded to a constant string. If more than one table/ColumnVisibilityButton is rendered on the same page, this will produce duplicate element IDs and ambiguous aria-controls references, which is an accessibility issue. Use a per-instance id (e.g., React useId() or derive from a unique table identifier).

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess two Table components can be shown at the same time, so this is valid.

  const menuId = 'column-visibility-menu' + React.useId();

Comment on lines +73 to +81
<MenuItem
disabled={!table.getIsSomeColumnsVisible()}
onClick={() => handleToggleAll(false)}
>
{localization.hideAll}
</MenuItem>
<MenuItem disabled={table.getIsAllColumnsVisible()} onClick={() => handleToggleAll(true)}>
{localization.showAll}
</MenuItem>
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The disabled logic for Hide All / Show All uses table.getIsSomeColumnsVisible() and table.getIsAllColumnsVisible(), which considers all columns (including non-hideable/display columns). This can keep Hide All enabled (or Show All disabled) even when all columns in this menu are already hidden/visible. Consider computing the disabled states from the same columns list you toggle (e.g., hideableColumns.some(c => c.getIsVisible()) / hideableColumns.every(c => c.getIsVisible())).

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 138 out of 138 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

onChange={() => {}}
tabIndex={-1}
edge="start"
sx={{ mr: 1, pointerEvents: 'none' }}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The Switch component has an empty onChange handler and pointerEvents set to 'none', which is correct since the MenuItem handles the click. However, the Switch should not be in the tab order at all. Consider adding inputProps={{ 'aria-hidden': true }} to the Switch to ensure it's fully hidden from assistive technologies, since it's purely decorative and the MenuItem provides the actual interaction.

Suggested change
sx={{ mr: 1, pointerEvents: 'none' }}
sx={{ mr: 1, pointerEvents: 'none' }}
inputProps={{ 'aria-hidden': true }}

Copilot uses AI. Check for mistakes.
table: MRT_TableInstance<T>;
}) {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const menuId = 'column-visibility-menu';
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The menuId is hardcoded to a constant string 'column-visibility-menu'. Based on prior feedback, when multiple tables are rendered on the same page, this creates duplicate element IDs and breaks ARIA references. This was previously identified and a solution was suggested: use React.useId() to generate a unique ID per component instance.

Copilot uses AI. Check for mistakes.
style="margin-left: 8px; color: rgb(198, 40, 40);"
>
Active
Unreachable
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this make sense where this story was showing "Active" now it's showing "Unreachable"?

world
</p>
</div>
Failed to fetch release information
Copy link
Collaborator

@illume illume Feb 26, 2026

Choose a reason for hiding this comment

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

Before: "An update is available"
Now: "Failed to fetch release information"

Seems suspicious.

@yolossn yolossn force-pushed the table-a11y-column-selector branch from 741682a to fcc43ef Compare February 26, 2026 12:47
@sniok sniok force-pushed the headlamp-downstream branch from def6222 to 3ed87fa Compare February 26, 2026 15:35
Copilot AI review requested due to automatic review settings February 26, 2026 16:29
@sniok sniok force-pushed the table-a11y-column-selector branch from fcc43ef to 32dce31 Compare February 26, 2026 16:29
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

table: MRT_TableInstance<T>;
}) {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const menuId = 'column-visibility-menu';
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The menuId is still hardcoded to 'column-visibility-menu', which will cause duplicate element IDs if multiple tables are rendered on the same page. This was already identified in previous feedback. To fix this, import and use React's built-in useId hook (React 18+) to generate a unique ID per component instance. For example:

import { useId, useState } from 'react';
// ...
const menuId = useId();

This ensures each ColumnVisibilityButton instance has a unique menu ID, maintaining proper ARIA relationships and accessibility.

Copilot uses AI. Check for mistakes.
@illume illume added p0 Highest priority bug Something isn't working labels Feb 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a11y a11y related issues bug Something isn't working p0 Highest priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Accessibility team] Show/Hide Columns button controls are not accessible using keyboard in A11yCluster tab

4 participants