fix(table-chart): fix missing table header IDs#35968
Conversation
Table chart column headers were rendering with `id="header-undefined"` instead of meaningful IDs, breaking CSS customization and ARIA accessibility. **Root Cause:** PR #31590 (Ant Design v5) changed header IDs from `column.key` to `column.originalLabel`, but `originalLabel` is only set for time-comparison columns (added in PR #32665), leaving regular columns with undefined values. **Solution:** Implemented defensive fallback with sanitization: - Headers: `id="header-${sanitizeHeaderId(column.originalLabel || column.key)}"` - Cells: `aria-labelledby="header-${sanitizeHeaderId(column.originalLabel || column.key)}"` **Sanitization Required:** Column identifiers can contain CSS-unsafe characters (%, #, △, spaces), so added sanitization function to ensure valid CSS selectors and ARIA references. **Changes:** - Added `sanitizeHeaderId()` helper function - Updated header `id` attribute with fallback + sanitization - Updated cell `aria-labelledby` with fallback + sanitization - Added 2 comprehensive tests (regular + time-comparison columns) - Fixed test fixture mutation issue **Tests verify:** - No "undefined" in any header IDs - All IDs are CSS-safe (no spaces or special characters) - All ARIA relationships valid (cells reference existing headers) - Works for both regular and time-comparison columns Fixes #35783 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
**Performance improvement:** - Cache sanitized header ID value to avoid recomputing it twice per column - Compute once at column definition, use for both header `id` and cell `aria-labelledby` **Test coverage:** - Added 8 unit tests for `sanitizeHeaderId()` function - Tests verify all sanitization rules: %, #, △, spaces, and other special chars - Provides faster diagnostics if sanitization logic changes in future **Changes:** - Export `sanitizeHeaderId()` for testing - Cache sanitized value in `headerId` variable - Added comprehensive unit test suite Tests: 40 passed (8 new unit tests + 32 existing) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Code Review Agent Run #67d15cActionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
There was a problem hiding this comment.
Review by Korbit AI
Korbit automatically attempts to detect when you fix issues in new commits.
| Category | Issue | Status |
|---|---|---|
| Redundant header ID sanitization in render loop ▹ view | ||
| Inconsistent header ID generation fallback ▹ view |
Files scanned
| File Path | Reviewed |
|---|---|
| superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx | ✅ |
Explore our documentation to understand the languages and file types we support and the files we ignore.
Check out our docs on how you can make Korbit work best for you and your team.
|
🎪 Showtime deployed environment on GHA for 9fcddc5 • Environment: http://35.88.151.195:8080 (admin/admin) |
There was a problem hiding this comment.
Pull Request Overview
This PR adds header ID sanitization to the TableChart component to ensure HTML id attributes and ARIA labels are valid and safe for CSS selectors. The change prevents issues with special characters (%, #, △) and spaces in column identifiers that could break accessibility features and CSS selectors.
- Introduces
sanitizeHeaderId()function to replace problematic characters in header IDs - Updates header
idandaria-labelledbyattributes to use sanitized IDs - Adds comprehensive test coverage for the sanitization function and validates proper ARIA attribute handling
Reviewed Changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx | Implements sanitizeHeaderId() function and applies it to header and cell ARIA attributes |
| superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx | Adds unit tests for sanitizeHeaderId() and integration tests verifying proper header ID generation and ARIA attribute references |
|
|
||
| it('render cell without color', () => { | ||
| const dataWithEmptyCell = testData.advanced.queriesData[0]; | ||
| const dataWithEmptyCell = cloneDeep(testData.advanced.queriesData[0]); |
There was a problem hiding this comment.
[nitpick] Using cloneDeep here is appropriate to prevent test pollution by modifying the original test data. However, the variable name dataWithEmptyCell is misleading - based on the test context at line 507-510, this data will have a new row pushed to it, so it's not specifically 'empty cell data' but rather 'modified query data'. Consider renaming to modifiedQueryData or queryDataWithExtraRow for clarity.
There was a problem hiding this comment.
The name captures that we are testing empty cell rendering rather than the cloning + pushing. Will probably not change unless if we think it's a merge blocker.
… feedback Addresses code review feedback from PR #35968: - Collapse consecutive underscores in sanitized IDs for better readability - Use nullish coalescing (??) instead of OR (||) for more precise fallback logic - Add JSDoc documentation clarifying the need for ID prefixes - Add comprehensive unit tests for consecutive underscore handling These improvements enhance code quality and maintainability while preserving the existing functionality and passing all existing tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
|
🎪 Showtime deployed environment on GHA for f680611 • Environment: http://35.89.84.55:8080 (admin/admin) |
|
|
||
| it('render cell without color', () => { | ||
| const dataWithEmptyCell = testData.advanced.queriesData[0]; | ||
| const dataWithEmptyCell = cloneDeep(testData.advanced.queriesData[0]); |
There was a problem hiding this comment.
Using cloneDeep prevents mutation of shared test data, but this may create a shallow reference issue. The variable dataWithEmptyCell is assigned a deep clone of queriesData[0], but then the code pushes to dataWithEmptyCell.data which should work correctly. However, the subsequent usage in transformProps should verify this clone is used. Please verify the test props construction uses this cloned data rather than the original testData.advanced.
There was a problem hiding this comment.
The cloned data is used in the test, prevents mutation and things are working correctly. I don't thin this is a release blocker unless if the test becomes flakey.
| export function sanitizeHeaderId(columnId: string): string { | ||
| return ( | ||
| columnId | ||
| // Semantic replacements first: preserve meaning in IDs for readability | ||
| // (e.g., 'percentpct_nice' instead of '_pct_nice') | ||
| .replace(/%/g, 'percent') | ||
| .replace(/#/g, 'hash') | ||
| .replace(/△/g, 'delta') | ||
| // Generic sanitization for remaining special characters | ||
| .replace(/\s+/g, '_') | ||
| .replace(/[^a-zA-Z0-9_-]/g, '_') | ||
| .replace(/_+/g, '_') // Collapse consecutive underscores | ||
| .replace(/^_+|_+$/g, '') // Trim leading/trailing underscores | ||
| ); | ||
| } |
There was a problem hiding this comment.
The sanitization logic has an ordering issue. Line 163 replaces all non-alphanumeric characters (except underscore and hyphen) with underscores, which means semantic replacements from lines 158-160 can introduce characters that are then immediately replaced. For example, sanitizeHeaderId('%') replaces % with 'percent' (✓), but if a column name is '%', it becomes 'percent' correctly. However, consider '%@': it becomes 'percent@' → 'percent_' → 'percent'. This works, but the flow could be clearer. The logic is correct but the comment on line 157 is misleading - it says the semantic replacement avoids '_pct_nice', but actually '%pct_nice' → 'percentpct_nice' → 'percentpct_nice' (no underscores between 'percent' and 'pct'). Consider updating the comment to accurately reflect the behavior.
| expect(sanitizeHeaderId('@@')).toBe(''); | ||
| expect(sanitizeHeaderId(' ')).toBe(''); | ||
| expect(sanitizeHeaderId('!@$')).toBe(''); | ||
| expect(sanitizeHeaderId('!@#$')).toBe('hash'); // # → 'hash' is preserved (semantic replacement) |
There was a problem hiding this comment.
The comment states '# → 'hash' is preserved (semantic replacement)' but this is misleading. The # is not 'preserved' - it's replaced with the word 'hash'. The comment should say '# → 'hash' replacement' or '# is replaced with 'hash' (semantic replacement)'.
| expect(sanitizeHeaderId('!@#$')).toBe('hash'); // # → 'hash' is preserved (semantic replacement) | |
| expect(sanitizeHeaderId('!@#$')).toBe('hash'); // # is replaced with 'hash' (semantic replacement) |
|
🎪 Showtime deployed environment on GHA for 5407932 • Environment: http://44.252.109.198:8080 (admin/admin) |
Addresses additional code review feedback:
- Fix misleading comment in sanitizeHeaderId: clarify transformation example
('percentpct_nice' → '%pct_nice' → 'percentpct_nice' for accuracy)
- Fix misleading use of "preserved" in test comments (characters are
REPLACED with words, not preserved)
- Replace all it() with test() for consistency with codebase standards
(30 instances updated to follow CLAUDE.md guidelines)
No functional changes - pure documentation and style improvements.
Tests: 45 passed (13 unit + 2 integration + 30 existing)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
|
🎪 Showtime deployed environment on GHA for ebfb2a9 • Environment: http://44.244.132.19:8080 (admin/admin) |
|
@rusackas wondering if one of your changes/rules didn't get updated. I had added |
SUMMARY
Table chart column headers were rendering with
id="header-undefined"instead of meaningful IDs, breaking CSS customization and ARIA accessibility that worked in Superset 4.1.1.Root Cause:
PR #31590 (Ant Design v5 migration) changed header IDs from
column.keytocolumn.originalLabel, butoriginalLabelis only set for time-comparison columns (added in PR #32665), leaving regular columns with undefined values.Solution:
Implemented defensive fallback with sanitization:
id="header-${sanitizeHeaderId(column.originalLabel || column.key)}"aria-labelledby="header-${sanitizeHeaderId(column.originalLabel || column.key)}"Column identifiers can contain CSS-unsafe characters (%, #, △, spaces), so added
sanitizeHeaderId()helper to ensure valid CSS selectors and ARIA references.BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
Before:

After:

TESTING INSTRUCTIONS
Manual Testing:
Test regular table chart:
Test time-comparison table chart:
Test special characters:
Test CSS customization (issue reporter's use case):
Automated Testing:
ADDITIONAL INFORMATION