-
Notifications
You must be signed in to change notification settings - Fork 3
Column Menu & Sort Features #147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Column Menu & Sort Features #147
Conversation
9c7e160
to
92578c0
Compare
Super cool thanks @rembrandtreyes! I'll review tonight. |
eslint.config.js
Outdated
import prettierPlugin from 'eslint-plugin-prettier' | ||
import prettierConfig from 'eslint-config-prettier' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like prettier, I'm open to considering it. I'm not at all happy with the current eslint situation. But the change belongs in its own PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah sounds good, I'll revert, and create an issue to add prettier in.
@@ -0,0 +1,14 @@ | |||
.headerText { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@severo can say more but... we've been trying to use existing elements and semantic tags (aria-, data-) to style as much as possible in hightable, avoiding adding classNames as much as possible.
The problem is that people want to custom style the table. But css modules obfuscate the classNames. So to expose it outside the component you either need to allow user to pass in custom classes that are populated through the table.
The (better, so far) alternative has been to let the user specify a custom top-level className (eg- className={styles.myhightable}
). Then if all the elements have semantic structure you can style from outside like:
.myhightable thead th > span {
text-overflow: ellipsis;
...
}
So basically I'm saying it would be better to do that here, so that users can see how we styled it, and not have to figure out weird css-affinity issues.
<ColumnMenuButton onClick={handleMenuButtonClick} /> | ||
<ColumnResizer setWidth={setWidth} onDoubleClick={autoResize} width={width} /> | ||
</th> | ||
<ColumnMenu |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason the ColumnMenu is a sibling of the <th>
? Why not inside the <th>
?
I'm not necessarily opposed but doesn't this result in table that looks like:
<table>
<thead>
<th>...</th>
<ColumnMenu/>
<th>...</th>
<ColumnMenu/>
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's the createPortal
implementation that we have in ColumnMenu. So the Column menu is attached to document.body instead, this is to prevent z-index issues.
I'll move this out into a function to render here instead and I'll add a comment.
|
||
if (!isVisible) return null | ||
|
||
return createPortal( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice
position: relative; | ||
display: inline-block; | ||
margin-left: 12px; | ||
font-size: 14px; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
normally would say don't use px for font-size. This is effectively an icon so I think it's okay. But if someone scale up text, should the sort indicator scale with the text?
PS- It looks really nice!
.map((column, index) => ({ column, index })) | ||
.filter(({ index }) => isColumnVisible(index)) | ||
.map(({ column, index: columnIndex }) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At first I was going to comment on this map/filter/map seems redundant. But as I was writing it I understood that you need the index to be stable even after the filter. So you're right. But maybe add a comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I wasn't sure of a "better" way to handle column visibility while preserving the original indexes. Good news is that it's still O(n) and after looking at the first map().filter() piece we can useMemo
to prevent unnecessary recalculations. The bad news is trying to grok this, and memory consideration with extremely large tables (column dependent).
I'll add these lines:
// We use map/filter/map to preserve original column indexes after filtering:
// 1. First map: Annotate each column with its original index
// 2. Filter: Remove hidden columns while keeping original index information
// 3. During render: Use the preserved originalIndex to access properties that
// depend on the column's position in the original array (CSS classes, event handlers, etc.)
if (sortDirection !== undefined) { | ||
// Handle explicit sort directions from the menu | ||
const { prefix, suffix } = partitionOrderBy(orderBy, columnHeader); | ||
|
||
if (sortDirection === null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's up with undefined
in one branch and null
in another?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sortDirection !== undefined
separates header clicks from column menu actions
(sortDirection === null)
handles the clear sort case within the column menu
I'll add better comments as it isn't very clear that the sortDirection
is undefined for header clicks.
onOrderByChange([{ column: columnHeader, direction: 'descending' }, ...suffix]); | ||
} else { | ||
// descending -> none | ||
onOrderByChange([...suffix]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no semicolons please... npm run lint:fix
Co-authored-by: Kenny Daniel <[email protected]>
Co-authored-by: Kenny Daniel <[email protected]>
661a9ef
to
271c8c3
Compare
Hey folks. Just got back from vacation. I'll be working through these comments today and rerequest a review. Thanks again @severo for great feedback! |
…isabled prop for better control over column visibility. Refactor styles and logic for improved clarity and functionality in the column menu.
…pdate HighTableContext to provide a default portal target. Simplify TableHeader by removing unnecessary hasHiddenColumns logic.
…y sorting functionality by replacing legacy props with a single onSort prop. Simplify event handling for sorting actions and enhance clarity in column visibility management.
…tions. Remove legacy sorting props and ensure correct event handling for sort and clear sort functionalities.
…ing padding values and adding a ref prop for improved table reference handling.
…abIndex of the ColumnMenuButton to -1 for improved accessibility. Reorder ColumnMenuButton rendering in ColumnHeader for better structure.
…ity by renaming TableProps to TableHeaderProps and removing unnecessary comments. Adjust import statements for consistency.
…ed layout flexibility.
Ok, I was able to merge in latest master and work through the conflicts 😅. I think I have everything included. I tested what the UX is in master and compared it to my branch and everything seems at parity for keyboard nav. At least the unit tests are happy 😄 I did not include the ColumnMenu in the keyboard nav because I think that can be its own issue/PR. Just combing through my PR now and making sure everything is aligned. |
…nnecessary re-renders, enhancing performance through prop comparison.
Damn I keep forgetting to check |
… and ensure proper cleanup when the menu is closed.
It's ready for review, right? Looking at it |
…dren prop to prevent unnecessary re-renders.
Yeah its ready now |
After closing the menu clicking outside, the table is stuck: Screencast.From.2025-05-16.10-11-15.mp4
|
Screencast.From.2025-05-16.10-15-23.mp4 |
@severo just saw your update 😆 I just called autoresize() when we cycle through sort states. What do you think? autoresize.mov |
Hmmm, no, I think sorting and resizing should be independent. We persist the column sizes in local storage, and we don't want to autoresize when sorting, it's not related. We should leave enough space in CSS instead. |
@severo I just checked master and I am seeing the same issue reported about clicking outside the columns in the whitespace. It appears to be selecting the table scroll container? Only way I can "escape" the lock is tabbing to the next item. |
Indeed, I reproduce incoherencies in master. Thanks for checking, I'll take care of it in another PR. edit: I created #167 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we have several big features in this PR, and it's slowing us down.
Are you OK with opening new PRs instead, so we can focus on each one?
- Feature: column menu #129 - create a menu element with no new action (the only action would be: toggle column sort, which is a bit silly, but would allow to see if it works as expected). We would ensure the keyboard works as expected and the portal is working well.
- add actions related to sorting (sort ascending, descending, remove from sort) to the menu. Taking care of handling the sort in sort.ts
- Feature: hide columns #3 - add the feature to hide columns.
- Use new icons for sort? #136 - change the sort icons
Sorry for asking for that now, I should have asked before, to save time for everyone. But I think it will help a lot with the integration. Again: thanks for your time, the features are super useful!
@@ -569,6 +616,8 @@ export function HighTableInner({ | |||
</tbody> | |||
</table> | |||
</div> | |||
{/* puts a background behind the row labels column */} | |||
<div className={styles.mockRowLabel} style={cornerStyle}> </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it seems to be duplicated code (see two lines below)
const getOnOrderByClick = useCallback((columnHeader: string) => { | ||
if (!onOrderByChange || !orderBy) return undefined | ||
return () => { | ||
onOrderByChange(toggleColumn(columnHeader, orderBy)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment should be fixed.
This PR adds an interactive column menu and improved sorting functionality to enhance the table user experience.
Key Changes:
1. New Column Menu Component
2. Enhanced Column Headers
3. Improved Sort UX
4. Code Quality
5. Technical Architecture
This PR improves data exploration by making column operations more discoverable and providing clearer visual feedback for current table state.
Demo
Screen.Recording.2025-05-03.at.11.53.00.PM.mov
No hiding for column.length === 1