upstreamable: frontend: Table: Implement keyboard accessible column selector in ColumnVisibilityButton#316
Conversation
There was a problem hiding this comment.
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
ColumnVisibilityButtonthat renders column visibility controls asMenuItems for proper arrow-key navigation and Enter/Space activation. - Updated
Table.tsxto render MRT’s filter toggle buttons and the new column visibility button viarenderToolbarInternalActions. - 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.
frontend/src/components/common/Table/ColumnVisibilityButton.tsx
Outdated
Show resolved
Hide resolved
f52c08b to
fcc43ef
Compare
skoeva
left a comment
There was a problem hiding this comment.
looks like some of the snapshots are outdated
There was a problem hiding this comment.
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.
| initialState: initState, | ||
| } = tbl.options; | ||
|
|
||
| return ( | ||
| <> | ||
| {enableFilters && enableGlobalFilter && !initState?.showGlobalFilter && ( |
There was a problem hiding this comment.
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).
| initialState: initState, | |
| } = tbl.options; | |
| return ( | |
| <> | |
| {enableFilters && enableGlobalFilter && !initState?.showGlobalFilter && ( | |
| } = tbl.options; | |
| return ( | |
| <> | |
| {enableFilters && enableGlobalFilter && ( |
| 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} |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
I guess two Table components can be shown at the same time, so this is valid.
const menuId = 'column-visibility-menu' + React.useId();
| <MenuItem | ||
| disabled={!table.getIsSomeColumnsVisible()} | ||
| onClick={() => handleToggleAll(false)} | ||
| > | ||
| {localization.hideAll} | ||
| </MenuItem> | ||
| <MenuItem disabled={table.getIsAllColumnsVisible()} onClick={() => handleToggleAll(true)}> | ||
| {localization.showAll} | ||
| </MenuItem> |
There was a problem hiding this comment.
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())).
There was a problem hiding this comment.
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' }} |
There was a problem hiding this comment.
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.
| sx={{ mr: 1, pointerEvents: 'none' }} | |
| sx={{ mr: 1, pointerEvents: 'none' }} | |
| inputProps={{ 'aria-hidden': true }} |
| table: MRT_TableInstance<T>; | ||
| }) { | ||
| const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null); | ||
| const menuId = 'column-visibility-menu'; |
There was a problem hiding this comment.
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.
| style="margin-left: 8px; color: rgb(198, 40, 40);" | ||
| > | ||
| Active | ||
| Unreachable |
There was a problem hiding this comment.
Does this make sense where this story was showing "Active" now it's showing "Unreachable"?
| world | ||
| </p> | ||
| </div> | ||
| Failed to fetch release information |
There was a problem hiding this comment.
Before: "An update is available"
Now: "Failed to fetch release information"
Seems suspicious.
741682a to
fcc43ef
Compare
def6222 to
3ed87fa
Compare
…elector in ColumnVisibilityButton
fcc43ef to
32dce31
Compare
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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.
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.
Changes
How to test: